@aexol/spectral 0.7.7 → 0.8.0

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.
Files changed (221) hide show
  1. package/dist/agent/agents.js +4 -4
  2. package/dist/agent/index.js +24 -148
  3. package/dist/cli.js +25 -220
  4. package/dist/commands/serve.js +1 -1
  5. package/dist/extensions/spectral-vision-fallback.js +225 -0
  6. package/dist/mcp/agent-dir.js +1 -1
  7. package/dist/mcp/config.js +3 -3
  8. package/dist/mcp/sampling-handler.js +1 -1
  9. package/dist/mcp/server-manager.js +5 -1
  10. package/dist/memory/commands/status.js +6 -6
  11. package/dist/memory/commands/view.js +16 -14
  12. package/dist/memory/compaction.js +33 -5
  13. package/dist/memory/config.js +3 -3
  14. package/dist/memory/debug-log.js +1 -1
  15. package/dist/memory/observer.js +2 -2
  16. package/dist/memory/prompts.js +5 -5
  17. package/dist/memory/tokens.js +1 -1
  18. package/dist/memory/tools/read-project-observations.js +2 -2
  19. package/dist/memory/tools/recall-observation.js +4 -4
  20. package/dist/relay/auto-research.js +23 -23
  21. package/dist/relay/dispatcher.js +28 -2
  22. package/dist/relay/models-fetch.js +15 -3
  23. package/dist/{pi → sdk}/coding-agent/cli/args.js +4 -4
  24. package/dist/{pi → sdk}/coding-agent/config.js +9 -20
  25. package/dist/{pi → sdk}/coding-agent/core/agent-session.js +5 -17
  26. package/dist/{pi → sdk}/coding-agent/core/compaction/compaction.js +161 -5
  27. package/dist/{pi → sdk}/coding-agent/core/extensions/loader.js +0 -6
  28. package/dist/{pi → sdk}/coding-agent/core/extensions/runner.js +7 -1
  29. package/dist/{pi → sdk}/coding-agent/core/keybindings.js +129 -2
  30. package/dist/{pi → sdk}/coding-agent/core/model-registry.js +11 -4
  31. package/dist/{pi → sdk}/coding-agent/core/package-manager.js +5 -5
  32. package/dist/{pi → sdk}/coding-agent/core/sdk.js +1 -1
  33. package/dist/{pi → sdk}/coding-agent/core/session-manager.js +4 -4
  34. package/dist/{pi → sdk}/coding-agent/core/settings-manager.js +20 -0
  35. package/dist/{pi → sdk}/coding-agent/core/telemetry.js +1 -1
  36. package/dist/{pi → sdk}/coding-agent/core/tools/bash.js +17 -63
  37. package/dist/{pi → sdk}/coding-agent/core/tools/edit.js +4 -141
  38. package/dist/{pi → sdk}/coding-agent/core/tools/find.js +0 -11
  39. package/dist/{pi → sdk}/coding-agent/core/tools/grep.js +0 -11
  40. package/dist/{pi → sdk}/coding-agent/core/tools/ls.js +0 -11
  41. package/dist/{pi → sdk}/coding-agent/core/tools/read.js +0 -12
  42. package/dist/{pi → sdk}/coding-agent/core/tools/render-utils.js +1 -14
  43. package/dist/{pi → sdk}/coding-agent/core/tools/write.js +2 -97
  44. package/dist/{pi → sdk}/coding-agent/migrations.js +3 -3
  45. package/dist/{pi → sdk}/coding-agent/modes/interactive/components/keybinding-hints.js +1 -1
  46. package/dist/sdk/coding-agent/modes/interactive/components/visual-truncate.js +26 -0
  47. package/dist/{pi → sdk}/coding-agent/modes/interactive/theme/theme.js +1 -2
  48. package/dist/{pi → sdk}/coding-agent/utils/tools-manager.js +1 -1
  49. package/dist/{pi → sdk}/coding-agent/utils/version-check.js +2 -2
  50. package/dist/{pi → sdk}/coding-agent/utils/windows-self-update.js +1 -1
  51. package/dist/server/{pi-bridge.js → agent-bridge.js} +158 -89
  52. package/dist/server/handlers/sessions.js +21 -0
  53. package/dist/server/session-stream.js +12 -6
  54. package/package.json +6 -3
  55. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +0 -248
  56. package/dist/pi/coding-agent/core/export-html/index.js +0 -225
  57. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +0 -107
  58. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +0 -32
  59. package/dist/pi/tui/autocomplete.js +0 -631
  60. package/dist/pi/tui/components/box.js +0 -103
  61. package/dist/pi/tui/components/cancellable-loader.js +0 -34
  62. package/dist/pi/tui/components/editor.js +0 -1915
  63. package/dist/pi/tui/components/image.js +0 -88
  64. package/dist/pi/tui/components/input.js +0 -425
  65. package/dist/pi/tui/components/loader.js +0 -68
  66. package/dist/pi/tui/components/markdown.js +0 -633
  67. package/dist/pi/tui/components/select-list.js +0 -158
  68. package/dist/pi/tui/components/settings-list.js +0 -184
  69. package/dist/pi/tui/components/spacer.js +0 -22
  70. package/dist/pi/tui/components/text.js +0 -88
  71. package/dist/pi/tui/components/truncated-text.js +0 -50
  72. package/dist/pi/tui/editor-component.js +0 -1
  73. package/dist/pi/tui/fuzzy.js +0 -109
  74. package/dist/pi/tui/index.js +0 -31
  75. package/dist/pi/tui/keybindings.js +0 -173
  76. package/dist/pi/tui/keys.js +0 -1172
  77. package/dist/pi/tui/kill-ring.js +0 -43
  78. package/dist/pi/tui/stdin-buffer.js +0 -360
  79. package/dist/pi/tui/terminal-image.js +0 -335
  80. package/dist/pi/tui/terminal.js +0 -324
  81. package/dist/pi/tui/tui.js +0 -1076
  82. package/dist/pi/tui/undo-stack.js +0 -24
  83. package/dist/pi/tui/utils.js +0 -1016
  84. /package/dist/{pi → sdk}/agent-core/agent-loop.js +0 -0
  85. /package/dist/{pi → sdk}/agent-core/agent.js +0 -0
  86. /package/dist/{pi → sdk}/agent-core/harness/agent-harness.js +0 -0
  87. /package/dist/{pi → sdk}/agent-core/harness/compaction/branch-summarization.js +0 -0
  88. /package/dist/{pi → sdk}/agent-core/harness/compaction/compaction.js +0 -0
  89. /package/dist/{pi → sdk}/agent-core/harness/compaction/utils.js +0 -0
  90. /package/dist/{pi → sdk}/agent-core/harness/env/nodejs.js +0 -0
  91. /package/dist/{pi → sdk}/agent-core/harness/messages.js +0 -0
  92. /package/dist/{pi → sdk}/agent-core/harness/prompt-templates.js +0 -0
  93. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-repo.js +0 -0
  94. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-storage.js +0 -0
  95. /package/dist/{pi → sdk}/agent-core/harness/session/memory-repo.js +0 -0
  96. /package/dist/{pi → sdk}/agent-core/harness/session/memory-storage.js +0 -0
  97. /package/dist/{pi → sdk}/agent-core/harness/session/repo-utils.js +0 -0
  98. /package/dist/{pi → sdk}/agent-core/harness/session/session.js +0 -0
  99. /package/dist/{pi → sdk}/agent-core/harness/session/uuid.js +0 -0
  100. /package/dist/{pi → sdk}/agent-core/harness/skills.js +0 -0
  101. /package/dist/{pi → sdk}/agent-core/harness/system-prompt.js +0 -0
  102. /package/dist/{pi → sdk}/agent-core/harness/types.js +0 -0
  103. /package/dist/{pi → sdk}/agent-core/harness/utils/shell-output.js +0 -0
  104. /package/dist/{pi → sdk}/agent-core/harness/utils/truncate.js +0 -0
  105. /package/dist/{pi → sdk}/agent-core/index.js +0 -0
  106. /package/dist/{pi → sdk}/agent-core/node.js +0 -0
  107. /package/dist/{pi → sdk}/agent-core/proxy.js +0 -0
  108. /package/dist/{pi → sdk}/agent-core/types.js +0 -0
  109. /package/dist/{pi → sdk}/ai/api-registry.js +0 -0
  110. /package/dist/{pi → sdk}/ai/cli.js +0 -0
  111. /package/dist/{pi → sdk}/ai/env-api-keys.js +0 -0
  112. /package/dist/{pi → sdk}/ai/image-models.generated.js +0 -0
  113. /package/dist/{pi → sdk}/ai/image-models.js +0 -0
  114. /package/dist/{pi → sdk}/ai/images-api-registry.js +0 -0
  115. /package/dist/{pi → sdk}/ai/images.js +0 -0
  116. /package/dist/{pi → sdk}/ai/index.js +0 -0
  117. /package/dist/{pi → sdk}/ai/models.generated.js +0 -0
  118. /package/dist/{pi → sdk}/ai/models.js +0 -0
  119. /package/dist/{pi → sdk}/ai/oauth.js +0 -0
  120. /package/dist/{pi → sdk}/ai/providers/anthropic.js +0 -0
  121. /package/dist/{pi → sdk}/ai/providers/faux.js +0 -0
  122. /package/dist/{pi → sdk}/ai/providers/github-copilot-headers.js +0 -0
  123. /package/dist/{pi → sdk}/ai/providers/openai-completions.js +0 -0
  124. /package/dist/{pi → sdk}/ai/providers/openai-prompt-cache.js +0 -0
  125. /package/dist/{pi → sdk}/ai/providers/register-builtins.js +0 -0
  126. /package/dist/{pi → sdk}/ai/providers/simple-options.js +0 -0
  127. /package/dist/{pi → sdk}/ai/providers/transform-messages.js +0 -0
  128. /package/dist/{pi → sdk}/ai/session-resources.js +0 -0
  129. /package/dist/{pi → sdk}/ai/stream.js +0 -0
  130. /package/dist/{pi → sdk}/ai/types.js +0 -0
  131. /package/dist/{pi → sdk}/ai/utils/diagnostics.js +0 -0
  132. /package/dist/{pi → sdk}/ai/utils/event-stream.js +0 -0
  133. /package/dist/{pi → sdk}/ai/utils/hash.js +0 -0
  134. /package/dist/{pi → sdk}/ai/utils/headers.js +0 -0
  135. /package/dist/{pi → sdk}/ai/utils/json-parse.js +0 -0
  136. /package/dist/{pi → sdk}/ai/utils/node-http-proxy.js +0 -0
  137. /package/dist/{pi → sdk}/ai/utils/oauth/anthropic.js +0 -0
  138. /package/dist/{pi → sdk}/ai/utils/oauth/device-code.js +0 -0
  139. /package/dist/{pi → sdk}/ai/utils/oauth/github-copilot.js +0 -0
  140. /package/dist/{pi → sdk}/ai/utils/oauth/index.js +0 -0
  141. /package/dist/{pi → sdk}/ai/utils/oauth/oauth-page.js +0 -0
  142. /package/dist/{pi → sdk}/ai/utils/oauth/openai-codex.js +0 -0
  143. /package/dist/{pi → sdk}/ai/utils/oauth/pkce.js +0 -0
  144. /package/dist/{pi → sdk}/ai/utils/oauth/types.js +0 -0
  145. /package/dist/{pi → sdk}/ai/utils/overflow.js +0 -0
  146. /package/dist/{pi → sdk}/ai/utils/sanitize-unicode.js +0 -0
  147. /package/dist/{pi → sdk}/ai/utils/typebox-helpers.js +0 -0
  148. /package/dist/{pi → sdk}/ai/utils/validation.js +0 -0
  149. /package/dist/{pi → sdk}/coding-agent/bun/cli.js +0 -0
  150. /package/dist/{pi → sdk}/coding-agent/bun/restore-sandbox-env.js +0 -0
  151. /package/dist/{pi → sdk}/coding-agent/cli/file-processor.js +0 -0
  152. /package/dist/{pi → sdk}/coding-agent/cli/initial-message.js +0 -0
  153. /package/dist/{pi → sdk}/coding-agent/cli.js +0 -0
  154. /package/dist/{pi → sdk}/coding-agent/core/agent-session-runtime.js +0 -0
  155. /package/dist/{pi → sdk}/coding-agent/core/agent-session-services.js +0 -0
  156. /package/dist/{pi → sdk}/coding-agent/core/auth-guidance.js +0 -0
  157. /package/dist/{pi → sdk}/coding-agent/core/auth-storage.js +0 -0
  158. /package/dist/{pi → sdk}/coding-agent/core/bash-executor.js +0 -0
  159. /package/dist/{pi → sdk}/coding-agent/core/compaction/branch-summarization.js +0 -0
  160. /package/dist/{pi → sdk}/coding-agent/core/compaction/index.js +0 -0
  161. /package/dist/{pi → sdk}/coding-agent/core/compaction/utils.js +0 -0
  162. /package/dist/{pi → sdk}/coding-agent/core/defaults.js +0 -0
  163. /package/dist/{pi → sdk}/coding-agent/core/diagnostics.js +0 -0
  164. /package/dist/{pi → sdk}/coding-agent/core/event-bus.js +0 -0
  165. /package/dist/{pi → sdk}/coding-agent/core/exec.js +0 -0
  166. /package/dist/{pi → sdk}/coding-agent/core/extensions/index.js +0 -0
  167. /package/dist/{pi → sdk}/coding-agent/core/extensions/types.js +0 -0
  168. /package/dist/{pi → sdk}/coding-agent/core/extensions/wrapper.js +0 -0
  169. /package/dist/{pi → sdk}/coding-agent/core/footer-data-provider.js +0 -0
  170. /package/dist/{pi → sdk}/coding-agent/core/http-dispatcher.js +0 -0
  171. /package/dist/{pi → sdk}/coding-agent/core/index.js +0 -0
  172. /package/dist/{pi → sdk}/coding-agent/core/messages.js +0 -0
  173. /package/dist/{pi → sdk}/coding-agent/core/model-resolver.js +0 -0
  174. /package/dist/{pi → sdk}/coding-agent/core/output-guard.js +0 -0
  175. /package/dist/{pi → sdk}/coding-agent/core/prompt-templates.js +0 -0
  176. /package/dist/{pi → sdk}/coding-agent/core/provider-display-names.js +0 -0
  177. /package/dist/{pi → sdk}/coding-agent/core/resolve-config-value.js +0 -0
  178. /package/dist/{pi → sdk}/coding-agent/core/resource-loader.js +0 -0
  179. /package/dist/{pi → sdk}/coding-agent/core/session-cwd.js +0 -0
  180. /package/dist/{pi → sdk}/coding-agent/core/skills.js +0 -0
  181. /package/dist/{pi → sdk}/coding-agent/core/slash-commands.js +0 -0
  182. /package/dist/{pi → sdk}/coding-agent/core/source-info.js +0 -0
  183. /package/dist/{pi → sdk}/coding-agent/core/system-prompt.js +0 -0
  184. /package/dist/{pi → sdk}/coding-agent/core/timings.js +0 -0
  185. /package/dist/{pi → sdk}/coding-agent/core/tools/edit-diff.js +0 -0
  186. /package/dist/{pi → sdk}/coding-agent/core/tools/file-mutation-queue.js +0 -0
  187. /package/dist/{pi → sdk}/coding-agent/core/tools/index.js +0 -0
  188. /package/dist/{pi → sdk}/coding-agent/core/tools/output-accumulator.js +0 -0
  189. /package/dist/{pi → sdk}/coding-agent/core/tools/path-utils.js +0 -0
  190. /package/dist/{pi → sdk}/coding-agent/core/tools/tool-definition-wrapper.js +0 -0
  191. /package/dist/{pi → sdk}/coding-agent/core/tools/truncate.js +0 -0
  192. /package/dist/{pi → sdk}/coding-agent/index.js +0 -0
  193. /package/dist/{pi → sdk}/coding-agent/main.js +0 -0
  194. /package/dist/{pi → sdk}/coding-agent/modes/index.js +0 -0
  195. /package/dist/{pi → sdk}/coding-agent/modes/interactive/components/diff.js +0 -0
  196. /package/dist/{pi → sdk}/coding-agent/modes/interactive/interactive-mode.js +0 -0
  197. /package/dist/{pi → sdk}/coding-agent/modes/print-mode.js +0 -0
  198. /package/dist/{pi → sdk}/coding-agent/modes/rpc/jsonl.js +0 -0
  199. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-client.js +0 -0
  200. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-mode.js +0 -0
  201. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-types.js +0 -0
  202. /package/dist/{pi → sdk}/coding-agent/utils/ansi.js +0 -0
  203. /package/dist/{pi → sdk}/coding-agent/utils/changelog.js +0 -0
  204. /package/dist/{pi → sdk}/coding-agent/utils/child-process.js +0 -0
  205. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-image.js +0 -0
  206. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-native.js +0 -0
  207. /package/dist/{pi → sdk}/coding-agent/utils/clipboard.js +0 -0
  208. /package/dist/{pi → sdk}/coding-agent/utils/exif-orientation.js +0 -0
  209. /package/dist/{pi → sdk}/coding-agent/utils/frontmatter.js +0 -0
  210. /package/dist/{pi → sdk}/coding-agent/utils/fs-watch.js +0 -0
  211. /package/dist/{pi → sdk}/coding-agent/utils/git.js +0 -0
  212. /package/dist/{pi → sdk}/coding-agent/utils/html.js +0 -0
  213. /package/dist/{pi → sdk}/coding-agent/utils/image-convert.js +0 -0
  214. /package/dist/{pi → sdk}/coding-agent/utils/image-resize.js +0 -0
  215. /package/dist/{pi → sdk}/coding-agent/utils/mime.js +0 -0
  216. /package/dist/{pi → sdk}/coding-agent/utils/paths.js +0 -0
  217. /package/dist/{pi → sdk}/coding-agent/utils/photon.js +0 -0
  218. /package/dist/{pi → sdk}/coding-agent/utils/pi-user-agent.js +0 -0
  219. /package/dist/{pi → sdk}/coding-agent/utils/shell.js +0 -0
  220. /package/dist/{pi → sdk}/coding-agent/utils/sleep.js +0 -0
  221. /package/dist/{pi → sdk}/coding-agent/utils/syntax-highlight.js +0 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Per-connection pi SDK lifecycle.
3
3
  *
4
- * One `PiBridge` instance per active WebSocket connection. Wraps:
4
+ * One `AgentBridge` instance per active WebSocket connection. Wraps:
5
5
  * - `createAgentSession` (in-memory session manager — we own persistence in
6
6
  * SQLite; pi doesn't need to write its own JSONL files).
7
7
  * - `subscribe` listener that translates pi `AgentSessionEvent`s into our
@@ -41,7 +41,7 @@
41
41
  * History rehydration:
42
42
  * On first attach to a previously-created session (e.g. after a server
43
43
  * restart), the SessionStreamManager passes the full SQLite transcript
44
- * to the PiBridge via `PiBridgeOptions.history`. Before
44
+ * to the AgentBridge via `AgentBridgeOptions.history`. Before
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
@@ -49,12 +49,13 @@
49
49
  * instance is reused across `prompt()` calls).
50
50
  */
51
51
  import { createJiti } from "@mariozechner/jiti";
52
- import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, } from "../pi/coding-agent/index.js";
52
+ import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, SettingsManager, } from "../sdk/coding-agent/index.js";
53
53
  import { randomUUID } from "node:crypto";
54
54
  import { existsSync, statSync } from "node:fs";
55
55
  import { dirname, join, resolve } from "node:path";
56
56
  import { fileURLToPath } from "node:url";
57
57
  import aexolMcpExtension from "../extensions/aexol-mcp.js";
58
+ import spectralVisionExtension from "../extensions/spectral-vision-fallback.js";
58
59
  import subagentExt from "../agent/index.js";
59
60
  import designerExtension from "../designer/index.js";
60
61
  import observationalMemory from "../memory/index.js";
@@ -133,7 +134,7 @@ function extractTextFromContent(content) {
133
134
  */
134
135
  function resolveMcpAdapterEntry() {
135
136
  const __dirname = dirname(fileURLToPath(import.meta.url));
136
- // pi-bridge.ts compiles to dist/server/pi-bridge.js;
137
+ // agent-bridge.ts compiles to dist/server/agent-bridge.js;
137
138
  // the bundled MCP adapter sits next door at dist/mcp/index.js.
138
139
  const bundledIndex = resolve(__dirname, "..", "mcp", "index.js");
139
140
  if (existsSync(bundledIndex))
@@ -195,6 +196,27 @@ function bareModelId(modelId) {
195
196
  const idx = modelId.lastIndexOf("/");
196
197
  return idx === -1 ? modelId : modelId.slice(idx + 1);
197
198
  }
199
+ /** Get model cost from backend credit rates, falling back to hardcoded pricing table. */
200
+ function getModelCost(m) {
201
+ // Prefer backend credit rates when configured
202
+ const hasCredits = m.creditInputPer1M != null ||
203
+ m.creditOutputPer1M != null ||
204
+ m.creditCacheReadPer1M != null ||
205
+ m.creditCacheWritePer1M != null;
206
+ if (hasCredits) {
207
+ return {
208
+ input: m.creditInputPer1M ?? 0,
209
+ output: m.creditOutputPer1M ?? 0,
210
+ cacheRead: m.creditCacheReadPer1M ?? 0,
211
+ cacheWrite: m.creditCacheWritePer1M ?? 0,
212
+ };
213
+ }
214
+ // Fall back to hardcoded pricing table when credits aren't configured
215
+ const pricing = lookupPricing(m.modelId);
216
+ return pricing
217
+ ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
218
+ : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
219
+ }
198
220
  /** Look up pricing for a modelId. Returns null when unknown. */
199
221
  function lookupPricing(modelId) {
200
222
  const bare = bareModelId(modelId);
@@ -210,21 +232,28 @@ function lookupPricing(modelId) {
210
232
  }
211
233
  return null;
212
234
  }
213
- /**
214
- * Model prefixes known to support reasoning/thinking.
215
- * Mirrors pi-ai's supportsXhigh() + additional models.
216
- */
217
- const REASONING_SUPPORT_PREFIXES = [
218
- "gpt-5.2", "gpt-5.3", "gpt-5.4", "gpt-5.5",
219
- "claude-opus-4",
220
- "o3", "o4",
221
- "deepseek-r1",
222
- "gemini-2.5",
223
- ];
224
- /** Check if a modelId prefix indicates reasoning/thinking support. */
225
- function supportsReasoning(modelId) {
226
- const bare = bareModelId(modelId);
227
- return REASONING_SUPPORT_PREFIXES.some((p) => modelId.startsWith(p) || bare.startsWith(p));
235
+ function inferSyntheticOpenAICompat(model) {
236
+ const bare = bareModelId(model.modelId);
237
+ const isDeepSeek = model.provider === "deepseek" ||
238
+ model.modelId.startsWith("deepseek/") ||
239
+ bare.startsWith("deepseek");
240
+ if (isDeepSeek) {
241
+ return {
242
+ thinkingFormat: "deepseek",
243
+ requiresReasoningContentOnAssistantMessages: true,
244
+ supportsDeveloperRole: false,
245
+ };
246
+ }
247
+ if (model.provider === "openrouter") {
248
+ const compat = {
249
+ thinkingFormat: "openrouter",
250
+ };
251
+ if (model.modelId.startsWith("anthropic/")) {
252
+ compat.cacheControlFormat = "anthropic";
253
+ }
254
+ return compat;
255
+ }
256
+ return undefined;
228
257
  }
229
258
  /**
230
259
  * Calculate credits from token usage using per-model credit rates.
@@ -348,7 +377,7 @@ function toRestoredMemorySnapshot(snapshot) {
348
377
  details: normalizeSnapshotDetails(snapshot.details),
349
378
  };
350
379
  }
351
- export class PiBridge {
380
+ export class AgentBridge {
352
381
  session;
353
382
  sessionManager;
354
383
  unsubscribe;
@@ -388,8 +417,8 @@ export class PiBridge {
388
417
  */
389
418
  async start() {
390
419
  if (this.disposed)
391
- throw new Error("PiBridge already disposed");
392
- const extensionFactories = [aexolMcpExtension, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
420
+ throw new Error("AgentBridge already disposed");
421
+ const extensionFactories = [aexolMcpExtension, async (pi) => { spectralVisionExtension(pi); }, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
393
422
  // Load pi-mcp-adapter via jiti so tsc never crawls its .ts files in
394
423
  // node_modules. The static `import` was causing tsc to type-check
395
424
  // pi-mcp-adapter's source and fail the build on its type errors.
@@ -404,25 +433,25 @@ export class PiBridge {
404
433
  }
405
434
  }
406
435
  catch {
407
- console.info("[PiBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
436
+ console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
408
437
  }
409
438
  }
410
439
  else {
411
- console.info("[PiBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
440
+ console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
412
441
  }
413
442
  // ResourceLoader with extensions wired in via factories.
414
443
  // Each factory's signature `(pi: ExtensionAPI) => Promise<void>` matches
415
444
  // the ExtensionFactory type exactly, so we can pass them directly.
416
445
  //
417
- // Skill discovery: pi's defaults scan ~/.pi/agent/skills/ (user),
418
- // .pi/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
446
+ // Skill discovery: pi's defaults scan ~/.spectral/agent/skills/ (user),
447
+ // .spectral/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
419
448
  // (ancestor-walked). We additionally walk ancestors for
420
449
  // .opencode/skills and .aexol/skills so OpenCode/Codex/Aexol skills
421
450
  // work out of the box.
422
451
  const extraSkillPaths = collectAncestorSkillDirs(this.opts.cwd, [".opencode/skills", ".aexol/skills"]);
423
452
  const resourceLoader = new DefaultResourceLoader({
424
453
  cwd: this.opts.cwd,
425
- agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.pi/agent`,
454
+ agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.spectral/agent`,
426
455
  extensionFactories,
427
456
  noExtensions: false,
428
457
  noSkills: false,
@@ -528,7 +557,7 @@ export class PiBridge {
528
557
  }
529
558
  // system messages are informational only; skip for LLM context
530
559
  }
531
- console.info(`[PiBridge] Rehydrated ${this.opts.history.length} history message(s) into session context`);
560
+ console.info(`[AgentBridge] Rehydrated ${this.opts.history.length} history message(s) into session context`);
532
561
  }
533
562
  if (this.opts.memorySnapshot) {
534
563
  const restoredSnapshot = toRestoredMemorySnapshot(this.opts.memorySnapshot);
@@ -544,10 +573,10 @@ export class PiBridge {
544
573
  coveredSourceCount: restoredSnapshot.coveredSourceCount,
545
574
  });
546
575
  }
547
- console.info(`[PiBridge] Restored observational memory snapshot covering ${restoredSnapshot.coveredSourceCount} source entr${restoredSnapshot.coveredSourceCount === 1 ? "y" : "ies"}`);
576
+ console.info(`[AgentBridge] Restored observational memory snapshot covering ${restoredSnapshot.coveredSourceCount} source entr${restoredSnapshot.coveredSourceCount === 1 ? "y" : "ies"}`);
548
577
  }
549
- // Build a model registry that does NOT touch ~/.pi/agent/auth.json or
550
- // ~/.pi/agent/models.json — the backend is now the only source of
578
+ // Build a model registry that does NOT touch ~/.spectral/agent/auth.json or
579
+ // ~/.spectral/agent/models.json — the backend is now the only source of
551
580
  // provider credentials and the only allowed inference target. We then
552
581
  // register synthetic providers (`spectral-proxy-anthropic` /
553
582
  // `spectral-proxy-openai`) whose `baseUrl` points at the backend's
@@ -560,21 +589,34 @@ export class PiBridge {
560
589
  // disk-based auth: this is the single path for `spectral serve`.
561
590
  const authStorage = AuthStorage.inMemory();
562
591
  this.modelRegistry = ModelRegistry.inMemory(authStorage);
563
- const fetchModels = this.opts.fetchAllowedModels ?? defaultFetchAllowedModels;
564
592
  let allowedModels;
565
593
  try {
566
- allowedModels = await fetchModels({
567
- backendUrl: this.opts.backendUrl,
568
- machineJwt: this.opts.machineJwt,
569
- });
594
+ allowedModels = await this.refreshAllowedModels();
570
595
  }
571
596
  catch (err) {
572
597
  const e = err instanceof Error ? err : new Error(String(err));
573
598
  throw new Error(`Failed to fetch allowed models from backend; check SPECTRAL_BACKEND_URL ` +
574
599
  `and machine JWT. Underlying error: ${e.message}`);
575
600
  }
576
- this.allowedModels = allowedModels;
577
- this.registerSyntheticProviders(allowedModels);
601
+ // Build an in-memory SettingsManager seeded with admin-configured
602
+ // defaults from the backend. findInitialModel() will pick up the
603
+ // isDefault model; the vision extension can query isVisionDefault.
604
+ const settingsOverrides = {};
605
+ const defaultModel = allowedModels.find((m) => m.isDefault);
606
+ if (defaultModel) {
607
+ const proxyProvider = this.proxyProviderForAllowedModel(defaultModel);
608
+ settingsOverrides.defaultProvider = proxyProvider;
609
+ settingsOverrides.defaultModel = defaultModel.modelId;
610
+ console.info(`✓ Default model from backend: ${proxyProvider}/${defaultModel.modelId}`);
611
+ }
612
+ const defaultVisionModel = allowedModels.find((m) => m.isVisionDefault);
613
+ if (defaultVisionModel) {
614
+ const proxyProvider = this.proxyProviderForAllowedModel(defaultVisionModel);
615
+ settingsOverrides.defaultVisionProvider = proxyProvider;
616
+ settingsOverrides.defaultVisionModel = defaultVisionModel.modelId;
617
+ console.info(`✓ Default vision model from backend: ${proxyProvider}/${defaultVisionModel.modelId}`);
618
+ }
619
+ const settingsManager = SettingsManager.inMemory(settingsOverrides);
578
620
  console.info(`✓ Inference routed via backend proxy (${allowedModels.length} model(s) available)`);
579
621
  const result = await createAgentSession({
580
622
  cwd: this.opts.cwd,
@@ -582,6 +624,7 @@ export class PiBridge {
582
624
  sessionManager,
583
625
  authStorage,
584
626
  modelRegistry: this.modelRegistry,
627
+ settingsManager,
585
628
  });
586
629
  this.session = result.session;
587
630
  // Headless UI context: forwards extension notify() calls as wire events
@@ -601,15 +644,38 @@ export class PiBridge {
601
644
  // registration.
602
645
  try {
603
646
  await this.session.bindExtensions({ uiContext });
604
- console.info("[PiBridge] session_start emitted; extensions initialized.");
647
+ console.info("[AgentBridge] session_start emitted; extensions initialized.");
605
648
  }
606
649
  catch (err) {
607
650
  const msg = err instanceof Error ? err.message : String(err);
608
- console.warn(`[PiBridge] session_start failed (extension init error): ${msg}`);
651
+ console.warn(`[AgentBridge] session_start failed (extension init error): ${msg}`);
609
652
  }
610
653
  // Subscribe BEFORE any prompt fires.
611
654
  this.unsubscribe = this.session.subscribe((ev) => this.handleEvent(ev));
612
655
  }
656
+ proxyProviderForAllowedModel(model) {
657
+ if (model.provider === "anthropic") {
658
+ return SPECTRAL_PROXY_ANTHROPIC;
659
+ }
660
+ if (model.provider === "built-in") {
661
+ return SPECTRAL_PROXY_USER_MODEL;
662
+ }
663
+ return SPECTRAL_PROXY_OPENAI;
664
+ }
665
+ async refreshAllowedModels(opts) {
666
+ if (!this.modelRegistry) {
667
+ throw new Error("AgentBridge model registry unavailable");
668
+ }
669
+ const fetchModels = this.opts.fetchAllowedModels ?? defaultFetchAllowedModels;
670
+ const allowedModels = await fetchModels({
671
+ backendUrl: this.opts.backendUrl,
672
+ machineJwt: this.opts.machineJwt,
673
+ bypassCache: opts?.bypassCache,
674
+ });
675
+ this.allowedModels = allowedModels;
676
+ this.registerSyntheticProviders(allowedModels);
677
+ return allowedModels;
678
+ }
613
679
  /**
614
680
  * Register one synthetic provider per upstream API shape. Anthropic models
615
681
  * go to `${backendUrl}/v1/messages` (Messages API); everything else (OpenAI,
@@ -636,7 +702,6 @@ export class PiBridge {
636
702
  authHeader: true,
637
703
  api: "anthropic-messages",
638
704
  models: anthropicModels.map((m) => {
639
- const pricing = lookupPricing(m.modelId);
640
705
  return {
641
706
  id: m.modelId,
642
707
  name: m.displayName,
@@ -649,12 +714,9 @@ export class PiBridge {
649
714
  // at our synthetic proxy provider so auth resolves to the machine JWT.
650
715
  provider: SPECTRAL_PROXY_ANTHROPIC,
651
716
  baseUrl,
652
- reasoning: supportsReasoning(m.modelId),
717
+ reasoning: m.supportsReasoning ?? false,
653
718
  input: m.supportsImages !== false ? ["text", "image"] : ["text"],
654
- // Real pricing so pi can compute accurate token costs.
655
- cost: pricing
656
- ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
657
- : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
719
+ cost: getModelCost(m),
658
720
  contextWindow: m.contextWindow ?? 0,
659
721
  maxTokens: 0,
660
722
  };
@@ -668,7 +730,6 @@ export class PiBridge {
668
730
  authHeader: true,
669
731
  api: "openai-completions",
670
732
  models: openaiCompatModels.map((m) => {
671
- const pricing = lookupPricing(m.modelId);
672
733
  return {
673
734
  id: m.modelId,
674
735
  name: m.displayName,
@@ -679,14 +740,12 @@ export class PiBridge {
679
740
  // breaking auth lookup against our synthetic proxy provider.
680
741
  provider: SPECTRAL_PROXY_OPENAI,
681
742
  baseUrl,
682
- reasoning: supportsReasoning(m.modelId),
743
+ reasoning: m.supportsReasoning ?? false,
683
744
  input: m.supportsImages !== false ? ["text", "image"] : ["text"],
684
- // Real pricing so pi can compute accurate token costs.
685
- cost: pricing
686
- ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
687
- : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
745
+ cost: getModelCost(m),
688
746
  contextWindow: m.contextWindow ?? 0,
689
747
  maxTokens: 0,
748
+ compat: inferSyntheticOpenAICompat(m),
690
749
  };
691
750
  }),
692
751
  });
@@ -703,20 +762,18 @@ export class PiBridge {
703
762
  authHeader: true,
704
763
  api: "openai-completions",
705
764
  models: userModelEntries.map((m) => {
706
- const pricing = lookupPricing(m.modelId);
707
765
  return {
708
766
  id: m.modelId,
709
767
  name: m.displayName,
710
768
  api: "openai-completions",
711
769
  provider: SPECTRAL_PROXY_USER_MODEL,
712
770
  baseUrl,
713
- reasoning: supportsReasoning(m.modelId),
771
+ reasoning: m.supportsReasoning ?? false,
714
772
  input: m.supportsImages !== false ? ["text", "image"] : ["text"],
715
- cost: pricing
716
- ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
717
- : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
773
+ cost: getModelCost(m),
718
774
  contextWindow: m.contextWindow ?? 0,
719
775
  maxTokens: 0,
776
+ compat: inferSyntheticOpenAICompat(m),
720
777
  };
721
778
  }),
722
779
  });
@@ -794,7 +851,7 @@ export class PiBridge {
794
851
  if (!modelId)
795
852
  return true; // nothing to apply — pi keeps its current model
796
853
  if (!this.session)
797
- throw new Error("PiBridge.start() not called");
854
+ throw new Error("AgentBridge.start() not called");
798
855
  if (this.lastAppliedModelId === modelId)
799
856
  return true; // idempotent: same model already in effect
800
857
  if (!this.modelRegistry) {
@@ -807,13 +864,26 @@ export class PiBridge {
807
864
  });
808
865
  return false;
809
866
  }
810
- const model = this.modelRegistry
867
+ let model = this.modelRegistry
811
868
  .getAvailable()
812
869
  .find((m) => m.id === modelId);
870
+ let refreshError;
871
+ if (!model) {
872
+ try {
873
+ await this.refreshAllowedModels({ bypassCache: true });
874
+ model = this.modelRegistry
875
+ .getAvailable()
876
+ .find((m) => m.id === modelId);
877
+ }
878
+ catch (err) {
879
+ refreshError = err instanceof Error ? err : new Error(String(err));
880
+ this.opts.onError?.(refreshError);
881
+ }
882
+ }
813
883
  if (!model) {
814
884
  this.opts.emit({
815
885
  type: "error",
816
- message: `Unknown modelId "${modelId}" — not found in pi model registry`,
886
+ message: `Unknown modelId "${modelId}" — not found in pi model registry${refreshError ? ` after refresh: ${refreshError.message}` : ""}`,
817
887
  });
818
888
  return false;
819
889
  }
@@ -915,16 +985,14 @@ export class PiBridge {
915
985
  */
916
986
  async prompt(text, images) {
917
987
  if (!this.session)
918
- throw new Error("PiBridge.start() not called");
919
- // Check whether the currently active model supports image input.
920
- // When `supportsImages` is null/undefined (unknown), we are conservative
921
- // and convert images to text rather than risking a 400 error.
922
- const currentModel = this.lastAppliedModelId
923
- ? this.allowedModels?.find((m) => m.modelId === this.lastAppliedModelId)
924
- : undefined;
925
- const modelSupportsImages = currentModel?.supportsImages === true;
988
+ throw new Error("AgentBridge.start() not called");
989
+ // Always pass images through to the session.
990
+ // The spectral-vision-fallback extension intercepts images via the
991
+ // `context` event and replaces them with text descriptions using a
992
+ // vision-capable model BEFORE they reach the LLM. This means images
993
+ // work regardless of whether the main model supports them.
926
994
  try {
927
- if (images && images.length > 0 && modelSupportsImages) {
995
+ if (images && images.length > 0) {
928
996
  const imageContents = images.map((img) => ({
929
997
  type: "image",
930
998
  data: img.data,
@@ -932,20 +1000,6 @@ export class PiBridge {
932
1000
  }));
933
1001
  await this.session.prompt(text, { images: imageContents });
934
1002
  }
935
- else if (images && images.length > 0 && !modelSupportsImages) {
936
- // Model doesn't support images — convert them to text descriptions
937
- // so the conversation can continue instead of hanging.
938
- const imageDescriptions = images
939
- .map((img, i) => `[Image ${i + 1}: ${img.mimeType}, ${img.data.length.toLocaleString()} bytes base64]`)
940
- .join("\n");
941
- const augmentedText = `${text}\n\n---\nThe following image(s) were attached but the current model does not support image input:\n${imageDescriptions}\n(Describe what you see or ask the user to switch to a model that supports images.)`;
942
- this.opts.emit({
943
- type: "agent_notification",
944
- message: `The current model does not support image input. ${images.length} image(s) were converted to text descriptions.`,
945
- level: "warning",
946
- });
947
- await this.session.prompt(augmentedText);
948
- }
949
1003
  else {
950
1004
  await this.session.prompt(text);
951
1005
  }
@@ -967,7 +1021,7 @@ export class PiBridge {
967
1021
  */
968
1022
  async compact(customInstructions) {
969
1023
  if (!this.session)
970
- throw new Error("PiBridge.start() not called");
1024
+ throw new Error("AgentBridge.start() not called");
971
1025
  this.memoryPhase = "compacting";
972
1026
  try {
973
1027
  await this.session.compact(customInstructions);
@@ -1056,13 +1110,13 @@ export class PiBridge {
1056
1110
  e.type === "tool_call" ||
1057
1111
  e.type === "tool_result");
1058
1112
  if (!finalContent && !hasMeaningfulEvent) {
1059
- console.debug("[pi-bridge] skipping empty intermediate message");
1113
+ console.debug("[agent-bridge] skipping empty intermediate message");
1060
1114
  try {
1061
1115
  this.opts.onAssistantMessageSkipped?.(messageId);
1062
1116
  }
1063
1117
  catch (err) {
1064
1118
  const e = err instanceof Error ? err : new Error(String(err));
1065
- console.error(`[pi-bridge] onAssistantMessageSkipped failed: ${e.message}`);
1119
+ console.error(`[agent-bridge] onAssistantMessageSkipped failed: ${e.message}`);
1066
1120
  }
1067
1121
  return;
1068
1122
  }
@@ -1177,6 +1231,8 @@ export class PiBridge {
1177
1231
  const done = details.results.filter(r => r.exitCode !== undefined && r.exitCode !== -1).length;
1178
1232
  const running = total - done;
1179
1233
  const currentAgent = details.results.find(r => r.exitCode === -1 || r.exitCode === undefined)?.agent;
1234
+ // Extract streaming text from partialResult content (set by text_delta forwarding).
1235
+ const streamingText = extractTextFromContent(partialResult?.content);
1180
1236
  const se = {
1181
1237
  type: "subagent_progress",
1182
1238
  toolCallId: ev.toolCallId,
@@ -1190,6 +1246,7 @@ export class PiBridge {
1190
1246
  : mode === "chain"
1191
1247
  ? `Chain: step ${details.results.length}/${total}`
1192
1248
  : `Running ${currentAgent ?? "subagent"}...`,
1249
+ streamingText: mode === "single" ? streamingText : undefined,
1193
1250
  };
1194
1251
  try {
1195
1252
  this.opts.emit(se);
@@ -1387,6 +1444,15 @@ function detectMemorySystem(message) {
1387
1444
  // Keep generic observational-memory messages visible in the landing badge.
1388
1445
  return "memory_observer";
1389
1446
  }
1447
+ /**
1448
+ * Detect subsystem from notification messages by known prefix.
1449
+ * Returns undefined for generic extension messages without a matching prefix.
1450
+ */
1451
+ function detectNotifSystem(message) {
1452
+ if (message.startsWith("[spectral-vision]"))
1453
+ return "vision";
1454
+ return detectMemorySystem(message);
1455
+ }
1390
1456
  /**
1391
1457
  * Create a minimal ExtensionUIContext that forwards `notify()` calls as
1392
1458
  * `agent_notification` wire events. All other UI methods are no-ops —
@@ -1397,15 +1463,18 @@ function detectMemorySystem(message) {
1397
1463
  function createHeadlessUIContext(emit) {
1398
1464
  // Defer to a Proxy so we don't need to stub every method.
1399
1465
  // `notify` is the only method called by extensions in serve mode
1400
- // (observational memory, MCP status bar updates).
1466
+ // (observational memory, MCP status bar updates, spectral-vision).
1401
1467
  const handler = {
1402
1468
  get(_target, prop) {
1403
1469
  if (prop === "notify") {
1404
1470
  return (message, type) => {
1405
1471
  const level = type ?? "info";
1406
- const system = detectMemorySystem(message);
1472
+ const system = detectNotifSystem(message);
1407
1473
  if (system?.startsWith("memory_")) {
1408
- console.info(`[PiBridge][memory][${level}] ${message}`);
1474
+ console.info(`[AgentBridge][memory][${level}] ${message}`);
1475
+ }
1476
+ else if (system === "vision") {
1477
+ console.info(`[AgentBridge][vision][${level}] ${message}`);
1409
1478
  }
1410
1479
  emit({
1411
1480
  type: "agent_notification",
@@ -65,6 +65,27 @@ export async function handleCompactSession(store, manager, id) {
65
65
  }
66
66
  return { ok: true };
67
67
  }
68
+ /**
69
+ * Remember & delete: compact the session (which persists observations as
70
+ * project memory via the compaction hook), then delete the session.
71
+ *
72
+ * This gives the user a way to keep a session's reflections as durable
73
+ * cross-session memory even for short sessions that never hit the compaction
74
+ * threshold naturally.
75
+ */
76
+ export async function handleRememberAndDeleteSession(store, manager, id) {
77
+ const detail = store.getSession(id);
78
+ if (!detail)
79
+ throw new NotFoundError("Session not found");
80
+ try {
81
+ await manager.compactSession(id);
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ throw new BadRequestError(message);
86
+ }
87
+ return { ok: true };
88
+ }
68
89
  /**
69
90
  * Fork a session: create a new session copying all messages from the
70
91
  * source, with the `fork_compact_source_id` flag set so the
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Session-scoped streaming layer for `spectral serve`.
3
3
  *
4
- * Background: prior to this module each WebSocket owned its own `PiBridge`
4
+ * Background: prior to this module each WebSocket owned its own `AgentBridge`
5
5
  * instance and the routes layer enforced single-writer-wins (4001 eviction)
6
6
  * to keep that bridge unique per session. That model lost data on browser
7
7
  * refresh — the WS close torn down the pi process mid-stream, and a re-open
@@ -37,14 +37,14 @@
37
37
  * for now — streams accumulate for the lifetime of the server process.
38
38
  */
39
39
  import { randomUUID } from "node:crypto";
40
- import { PiBridge } from "./pi-bridge.js";
40
+ import { AgentBridge } from "./agent-bridge.js";
41
41
  import { getMemoryState, isSourceEntry, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../memory/branch.js";
42
42
  import { observationPoolTokens, renderSummary } from "../memory/compaction.js";
43
43
  import { loadConfig } from "../memory/config.js";
44
44
  import { setProjectObsStore } from "../memory/project-observations-store.js";
45
45
  import { estimateStringTokens } from "../memory/tokens.js";
46
46
  import { reflectionContent, reflectionId } from "../memory/types.js";
47
- const DEFAULT_BRIDGE_FACTORY = (args) => new PiBridge(args);
47
+ const DEFAULT_BRIDGE_FACTORY = (args) => new AgentBridge(args);
48
48
  /** Safety limit for autonomous loop iterations per session. */
49
49
  const MAX_LOOP_ITERATIONS = 100;
50
50
  /**
@@ -646,7 +646,7 @@ export class SessionStreamManager {
646
646
  assistantText: "",
647
647
  };
648
648
  // 4. Fire pi. `prompt` resolves on agent_end; errors are handled inside
649
- // PiBridge (it emits `error` for us). We don't await — broadcast is
649
+ // AgentBridge (it emits `error` for us). We don't await — broadcast is
650
650
  // driven by the bridge's emit callback.
651
651
  void stream.bridge.prompt(content, images);
652
652
  }
@@ -1127,7 +1127,7 @@ export class SessionStreamManager {
1127
1127
  }
1128
1128
  // Broadcast first, then maybe close out the turn. agent_end clears the
1129
1129
  // buffer because by that point the assistant message is already in
1130
- // SQLite (PiBridge calls onAssistantMessageComplete on message_end,
1130
+ // SQLite (AgentBridge calls onAssistantMessageComplete on message_end,
1131
1131
  // which fires before agent_end).
1132
1132
  //
1133
1133
  // Track context window state from token_usage events — the bridge emits
@@ -1173,6 +1173,10 @@ export class SessionStreamManager {
1173
1173
  prunePersistedHistoryAfterCompaction(this.store, stream.sessionId, stream.bridge);
1174
1174
  }
1175
1175
  persistObservationalMemorySnapshot(this.store, stream.sessionId, stream.bridge);
1176
+ // Drain the prompt queue after compaction finishes. During
1177
+ // compaction, auto-dequeue in agent_end is skipped to prevent the
1178
+ // lossy prompt() guard from dropping queued items.
1179
+ this.maybeAutoDequeue(stream);
1176
1180
  // After compaction the session context has been reduced; push updated
1177
1181
  // context-window stats to all subscribers so the frontend's context
1178
1182
  // bar refreshes immediately instead of waiting for the next turn.
@@ -1287,8 +1291,10 @@ export class SessionStreamManager {
1287
1291
  // pending, check the persistent prompt queue. If there's a queued
1288
1292
  // prompt, start it immediately without broadcasting agent_end —
1289
1293
  // the frontend transitions seamlessly to the next turn.
1294
+ // Skip if compaction is in-flight (e.g. fork-compact just started
1295
+ // above) — compaction_end will drain the queue when it finishes.
1290
1296
  if (!stream.loopActive || !stream.loopOriginalPrompt) {
1291
- if (this.maybeAutoDequeue(stream))
1297
+ if (!stream.compacting && this.maybeAutoDequeue(stream))
1292
1298
  return;
1293
1299
  }
1294
1300
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.7.7",
4
- "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
3
+ "version": "0.8.0",
4
+ "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "bin": {
@@ -30,7 +30,6 @@
30
30
  "coding-agent",
31
31
  "ai",
32
32
  "cli",
33
- "pi",
34
33
  "claude",
35
34
  "openai",
36
35
  "agent",
@@ -46,6 +45,10 @@
46
45
  "url": "https://gitlab.aexol.com/aexol/spectral/-/issues"
47
46
  },
48
47
  "license": "MIT",
48
+ "spectralConfig": {
49
+ "name": "spectral",
50
+ "configDir": ".spectral"
51
+ },
49
52
  "publishConfig": {
50
53
  "access": "public"
51
54
  },