@aexol/spectral 0.7.1 → 0.7.5
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/CHANGELOG.md +5 -0
- package/dist/agent/agents.js +1 -1
- package/dist/agent/index.js +199 -184
- package/dist/commands/serve.js +0 -3
- package/dist/designer/data/systems/renault/DESIGN.md +1 -1
- package/dist/designer/philosophies.js +668 -0
- package/dist/mcp/sampling-handler.js +1 -1
- package/dist/memory/commands/status.js +1 -1
- package/dist/memory/compaction.js +2 -2
- package/dist/memory/config.js +1 -1
- package/dist/memory/debug-log.js +1 -1
- package/dist/memory/hooks/compaction-hook.js +29 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/observer.js +2 -2
- package/dist/memory/project-observations-store.js +14 -0
- package/dist/memory/tokens.js +1 -1
- package/dist/memory/tools/read-project-observations.js +82 -0
- package/dist/memory/tools/recall-observation.js +2 -2
- package/dist/pi/agent-core/agent-loop.js +501 -0
- package/dist/pi/agent-core/agent.js +401 -0
- package/dist/pi/agent-core/harness/agent-harness.js +899 -0
- package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
- package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
- package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
- package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
- package/dist/pi/agent-core/harness/messages.js +101 -0
- package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
- package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
- package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
- package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
- package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
- package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
- package/dist/pi/agent-core/harness/session/session.js +196 -0
- package/dist/pi/agent-core/harness/session/uuid.js +49 -0
- package/dist/pi/agent-core/harness/skills.js +310 -0
- package/dist/pi/agent-core/harness/system-prompt.js +29 -0
- package/dist/pi/agent-core/harness/types.js +93 -0
- package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
- package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
- package/dist/pi/agent-core/index.js +24 -0
- package/dist/pi/agent-core/node.js +2 -0
- package/dist/pi/agent-core/proxy.js +277 -0
- package/dist/pi/agent-core/types.js +1 -0
- package/dist/pi/ai/api-registry.js +43 -0
- package/dist/pi/ai/cli.js +120 -0
- package/dist/pi/ai/env-api-keys.js +169 -0
- package/dist/pi/ai/image-models.generated.js +441 -0
- package/dist/pi/ai/image-models.js +22 -0
- package/dist/pi/ai/images-api-registry.js +21 -0
- package/dist/pi/ai/images.js +13 -0
- package/dist/pi/ai/index.js +18 -0
- package/dist/pi/ai/models.generated.js +16220 -0
- package/dist/pi/ai/models.js +70 -0
- package/dist/pi/ai/oauth.js +1 -0
- package/dist/pi/ai/providers/anthropic.js +945 -0
- package/dist/pi/ai/providers/faux.js +367 -0
- package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
- package/dist/pi/ai/providers/openai-completions.js +945 -0
- package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
- package/dist/pi/ai/providers/register-builtins.js +97 -0
- package/dist/pi/ai/providers/simple-options.js +40 -0
- package/dist/pi/ai/providers/transform-messages.js +183 -0
- package/dist/pi/ai/session-resources.js +21 -0
- package/dist/pi/ai/stream.js +26 -0
- package/dist/pi/ai/types.js +1 -0
- package/dist/pi/ai/utils/diagnostics.js +24 -0
- package/dist/pi/ai/utils/event-stream.js +80 -0
- package/dist/pi/ai/utils/hash.js +13 -0
- package/dist/pi/ai/utils/headers.js +7 -0
- package/dist/pi/ai/utils/json-parse.js +112 -0
- package/dist/pi/ai/utils/node-http-proxy.js +96 -0
- package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
- package/dist/pi/ai/utils/oauth/device-code.js +54 -0
- package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
- package/dist/pi/ai/utils/oauth/index.js +121 -0
- package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
- package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
- package/dist/pi/ai/utils/oauth/pkce.js +30 -0
- package/dist/pi/ai/utils/oauth/types.js +1 -0
- package/dist/pi/ai/utils/overflow.js +150 -0
- package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
- package/dist/pi/ai/utils/typebox-helpers.js +20 -0
- package/dist/pi/ai/utils/validation.js +280 -0
- package/dist/pi/coding-agent/bun/cli.js +7 -0
- package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
- package/dist/pi/coding-agent/cli/args.js +340 -0
- package/dist/pi/coding-agent/cli/file-processor.js +82 -0
- package/dist/pi/coding-agent/cli/initial-message.js +21 -0
- package/dist/pi/coding-agent/cli.js +17 -0
- package/dist/pi/coding-agent/config.js +414 -0
- package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
- package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
- package/dist/pi/coding-agent/core/agent-session.js +2498 -0
- package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
- package/dist/pi/coding-agent/core/auth-storage.js +441 -0
- package/dist/pi/coding-agent/core/bash-executor.js +110 -0
- package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
- package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
- package/dist/pi/coding-agent/core/compaction/index.js +6 -0
- package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
- package/dist/pi/coding-agent/core/defaults.js +1 -0
- package/dist/pi/coding-agent/core/diagnostics.js +1 -0
- package/dist/pi/coding-agent/core/event-bus.js +24 -0
- package/dist/pi/coding-agent/core/exec.js +74 -0
- package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
- package/dist/pi/coding-agent/core/export-html/index.js +225 -0
- package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
- package/dist/pi/coding-agent/core/extensions/index.js +8 -0
- package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
- package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
- package/dist/pi/coding-agent/core/extensions/types.js +44 -0
- package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
- package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
- package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
- package/dist/pi/coding-agent/core/index.js +11 -0
- package/dist/pi/coding-agent/core/keybindings.js +294 -0
- package/dist/pi/coding-agent/core/messages.js +122 -0
- package/dist/pi/coding-agent/core/model-registry.js +728 -0
- package/dist/pi/coding-agent/core/model-resolver.js +494 -0
- package/dist/pi/coding-agent/core/output-guard.js +58 -0
- package/dist/pi/coding-agent/core/package-manager.js +2020 -0
- package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
- package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
- package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
- package/dist/pi/coding-agent/core/resource-loader.js +733 -0
- package/dist/pi/coding-agent/core/sdk.js +282 -0
- package/dist/pi/coding-agent/core/session-cwd.js +37 -0
- package/dist/pi/coding-agent/core/session-manager.js +1146 -0
- package/dist/pi/coding-agent/core/settings-manager.js +794 -0
- package/dist/pi/coding-agent/core/skills.js +386 -0
- package/dist/pi/coding-agent/core/slash-commands.js +24 -0
- package/dist/pi/coding-agent/core/source-info.js +18 -0
- package/dist/pi/coding-agent/core/system-prompt.js +122 -0
- package/dist/pi/coding-agent/core/telemetry.js +8 -0
- package/dist/pi/coding-agent/core/timings.js +30 -0
- package/dist/pi/coding-agent/core/tools/bash.js +341 -0
- package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
- package/dist/pi/coding-agent/core/tools/edit.js +324 -0
- package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
- package/dist/pi/coding-agent/core/tools/find.js +297 -0
- package/dist/pi/coding-agent/core/tools/grep.js +303 -0
- package/dist/pi/coding-agent/core/tools/index.js +111 -0
- package/dist/pi/coding-agent/core/tools/ls.js +168 -0
- package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
- package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
- package/dist/pi/coding-agent/core/tools/read.js +288 -0
- package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
- package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
- package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
- package/dist/pi/coding-agent/core/tools/write.js +212 -0
- package/dist/pi/coding-agent/index.js +41 -0
- package/dist/pi/coding-agent/main.js +5 -0
- package/dist/pi/coding-agent/migrations.js +280 -0
- package/dist/pi/coding-agent/modes/index.js +7 -0
- package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
- package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
- package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
- package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
- package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
- package/dist/pi/coding-agent/modes/print-mode.js +130 -0
- package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
- package/dist/pi/coding-agent/utils/ansi.js +51 -0
- package/dist/pi/coding-agent/utils/changelog.js +86 -0
- package/dist/pi/coding-agent/utils/child-process.js +87 -0
- package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
- package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
- package/dist/pi/coding-agent/utils/clipboard.js +116 -0
- package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
- package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
- package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
- package/dist/pi/coding-agent/utils/git.js +162 -0
- package/dist/pi/coding-agent/utils/html.js +39 -0
- package/dist/pi/coding-agent/utils/image-convert.js +38 -0
- package/dist/pi/coding-agent/utils/image-resize.js +136 -0
- package/dist/pi/coding-agent/utils/mime.js +68 -0
- package/dist/pi/coding-agent/utils/paths.js +91 -0
- package/dist/pi/coding-agent/utils/photon.js +120 -0
- package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
- package/dist/pi/coding-agent/utils/shell.js +194 -0
- package/dist/pi/coding-agent/utils/sleep.js +16 -0
- package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
- package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
- package/dist/pi/coding-agent/utils/version-check.js +81 -0
- package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
- package/dist/pi/tui/autocomplete.js +631 -0
- package/dist/pi/tui/components/box.js +103 -0
- package/dist/pi/tui/components/cancellable-loader.js +34 -0
- package/dist/pi/tui/components/editor.js +1915 -0
- package/dist/pi/tui/components/image.js +88 -0
- package/dist/pi/tui/components/input.js +425 -0
- package/dist/pi/tui/components/loader.js +68 -0
- package/dist/pi/tui/components/markdown.js +633 -0
- package/dist/pi/tui/components/select-list.js +158 -0
- package/dist/pi/tui/components/settings-list.js +184 -0
- package/dist/pi/tui/components/spacer.js +22 -0
- package/dist/pi/tui/components/text.js +88 -0
- package/dist/pi/tui/components/truncated-text.js +50 -0
- package/dist/pi/tui/editor-component.js +1 -0
- package/dist/pi/tui/fuzzy.js +109 -0
- package/dist/pi/tui/index.js +31 -0
- package/dist/pi/tui/keybindings.js +173 -0
- package/dist/pi/tui/keys.js +1172 -0
- package/dist/pi/tui/kill-ring.js +43 -0
- package/dist/pi/tui/stdin-buffer.js +360 -0
- package/dist/pi/tui/terminal-image.js +335 -0
- package/dist/pi/tui/terminal.js +324 -0
- package/dist/pi/tui/tui.js +1076 -0
- package/dist/pi/tui/undo-stack.js +24 -0
- package/dist/pi/tui/utils.js +1016 -0
- package/dist/relay/dispatcher.js +30 -0
- package/dist/server/handlers/queue.js +52 -0
- package/dist/server/pi-bridge.js +9 -1
- package/dist/server/session-stream.js +76 -111
- package/dist/server/storage.js +154 -2
- package/dist/server/title-generator.js +14 -153
- package/package.json +24 -6
package/dist/relay/dispatcher.js
CHANGED
|
@@ -42,6 +42,7 @@ import { BadRequestError, NotFoundError } from "../server/handlers/errors.js";
|
|
|
42
42
|
import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
|
|
43
43
|
import { handleBindStudioProject, handleCreateProject, handleDeleteProject, handleListProjects, handleListSessionsByProject, handleUpdateProject, } from "../server/handlers/projects.js";
|
|
44
44
|
import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
|
|
45
|
+
import { handleClearPromptQueue, handleEnqueuePrompt, handleGetPromptQueue, handleRemovePrompt, } from "../server/handlers/queue.js";
|
|
45
46
|
import { shutdownState } from "../server/shutdown.js";
|
|
46
47
|
import { handleAutoResearch } from "./auto-research.js";
|
|
47
48
|
/**
|
|
@@ -133,6 +134,25 @@ export function matchRoute(method, path) {
|
|
|
133
134
|
return { route: "fork_session", id };
|
|
134
135
|
return null;
|
|
135
136
|
}
|
|
137
|
+
// /api/sessions/:id/queue and /api/sessions/:id/queue/:itemId
|
|
138
|
+
const queueMatch = /^\/api\/sessions\/([^/]+)\/queue(\/([^/]+))?$/.exec(cleanPath);
|
|
139
|
+
if (queueMatch) {
|
|
140
|
+
const id = decodeURIComponent(queueMatch[1]);
|
|
141
|
+
const itemId = queueMatch[3] ? decodeURIComponent(queueMatch[3]) : undefined;
|
|
142
|
+
if (!itemId) {
|
|
143
|
+
if (method === "GET")
|
|
144
|
+
return { route: "get_prompt_queue", id };
|
|
145
|
+
if (method === "POST")
|
|
146
|
+
return { route: "enqueue_prompt", id };
|
|
147
|
+
if (method === "DELETE")
|
|
148
|
+
return { route: "clear_prompt_queue", id };
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
if (method === "DELETE")
|
|
152
|
+
return { route: "remove_prompt", id, itemId };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
136
156
|
return null;
|
|
137
157
|
}
|
|
138
158
|
/**
|
|
@@ -334,6 +354,16 @@ async function dispatchRoute(match, body, deps) {
|
|
|
334
354
|
const prefix = match.query?.get("prefix") ?? "";
|
|
335
355
|
return handlePathAutocomplete(prefix);
|
|
336
356
|
}
|
|
357
|
+
case "enqueue_prompt":
|
|
358
|
+
return handleEnqueuePrompt(store, manager, id, asObject(body));
|
|
359
|
+
case "get_prompt_queue":
|
|
360
|
+
return handleGetPromptQueue(store, id);
|
|
361
|
+
case "remove_prompt":
|
|
362
|
+
handleRemovePrompt(store, manager, id, match.itemId ?? "");
|
|
363
|
+
return { ok: true };
|
|
364
|
+
case "clear_prompt_queue":
|
|
365
|
+
handleClearPromptQueue(store, manager, id);
|
|
366
|
+
return { ok: true };
|
|
337
367
|
}
|
|
338
368
|
}
|
|
339
369
|
/**
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure REST handlers for `/api/sessions/:sessionId/queue/*`.
|
|
3
|
+
*
|
|
4
|
+
* Same contract as sessions/projects handlers — pure functions that take
|
|
5
|
+
* `store` + `manager` dependencies, throw `BadRequestError`/`NotFoundError`
|
|
6
|
+
* on invalid input, and return typed responses.
|
|
7
|
+
*
|
|
8
|
+
* After mutating the queue, each handler calls `manager.pushQueueState()`
|
|
9
|
+
* to broadcast the updated queue to all subscribers via `queue_changed`.
|
|
10
|
+
*/
|
|
11
|
+
import { BadRequestError, NotFoundError } from "./errors.js";
|
|
12
|
+
export function handleEnqueuePrompt(store, manager, sessionId, body) {
|
|
13
|
+
if (typeof body.content !== "string" || !body.content.trim()) {
|
|
14
|
+
throw new BadRequestError("content (non-empty string) is required");
|
|
15
|
+
}
|
|
16
|
+
const session = store.getSession(sessionId);
|
|
17
|
+
if (!session)
|
|
18
|
+
throw new NotFoundError("Session not found");
|
|
19
|
+
const row = store.enqueuePrompt(sessionId, body.content);
|
|
20
|
+
manager.pushQueueState(sessionId);
|
|
21
|
+
return {
|
|
22
|
+
id: row.id,
|
|
23
|
+
content: row.content,
|
|
24
|
+
position: row.position,
|
|
25
|
+
createdAt: row.created_at,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function handleGetPromptQueue(store, sessionId) {
|
|
29
|
+
const session = store.getSession(sessionId);
|
|
30
|
+
if (!session)
|
|
31
|
+
throw new NotFoundError("Session not found");
|
|
32
|
+
return store.getPromptQueue(sessionId).map((r) => ({
|
|
33
|
+
id: r.id,
|
|
34
|
+
content: r.content,
|
|
35
|
+
position: r.position,
|
|
36
|
+
createdAt: r.created_at,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
export function handleRemovePrompt(store, manager, sessionId, itemId) {
|
|
40
|
+
const session = store.getSession(sessionId);
|
|
41
|
+
if (!session)
|
|
42
|
+
throw new NotFoundError("Session not found");
|
|
43
|
+
store.removePrompt(sessionId, itemId);
|
|
44
|
+
manager.pushQueueState(sessionId);
|
|
45
|
+
}
|
|
46
|
+
export function handleClearPromptQueue(store, manager, sessionId) {
|
|
47
|
+
const session = store.getSession(sessionId);
|
|
48
|
+
if (!session)
|
|
49
|
+
throw new NotFoundError("Session not found");
|
|
50
|
+
store.clearPromptQueue(sessionId);
|
|
51
|
+
manager.pushQueueState(sessionId);
|
|
52
|
+
}
|
package/dist/server/pi-bridge.js
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
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 "
|
|
52
|
+
import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, } from "../pi/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";
|
|
@@ -750,6 +750,14 @@ export class PiBridge {
|
|
|
750
750
|
getFirstAvailableModelId() {
|
|
751
751
|
return this.allowedModels?.[0]?.modelId;
|
|
752
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Return current session context usage from pi's built-in estimator.
|
|
755
|
+
* Used after compaction and session start to push updated context-window
|
|
756
|
+
* stats to the frontend without waiting for the next assistant turn.
|
|
757
|
+
*/
|
|
758
|
+
getContextUsage() {
|
|
759
|
+
return this.session?.getContextUsage();
|
|
760
|
+
}
|
|
753
761
|
getSessionBranch() {
|
|
754
762
|
return (this.sessionManager?.getBranch() ?? []);
|
|
755
763
|
}
|
|
@@ -41,9 +41,9 @@ import { PiBridge } from "./pi-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
|
+
import { setProjectObsStore } from "../memory/project-observations-store.js";
|
|
44
45
|
import { estimateStringTokens } from "../memory/tokens.js";
|
|
45
46
|
import { reflectionContent, reflectionId } from "../memory/types.js";
|
|
46
|
-
import { generateSessionTitle, isDefaultTitle, } from "./title-generator.js";
|
|
47
47
|
const DEFAULT_BRIDGE_FACTORY = (args) => new PiBridge(args);
|
|
48
48
|
/** Safety limit for autonomous loop iterations per session. */
|
|
49
49
|
const MAX_LOOP_ITERATIONS = 100;
|
|
@@ -233,17 +233,7 @@ export class SessionStreamManager {
|
|
|
233
233
|
machineJwt;
|
|
234
234
|
bridgeFactory;
|
|
235
235
|
agentDir;
|
|
236
|
-
titleLlmCall;
|
|
237
|
-
disableAutoTitle;
|
|
238
|
-
publishMetaEvent;
|
|
239
236
|
streams = new Map();
|
|
240
|
-
/**
|
|
241
|
-
* Sessions for which we've already attempted (or queued) auto-title
|
|
242
|
-
* generation in this server process. Per-process is intentional: a server
|
|
243
|
-
* restart resets the set, but `isDefaultTitle()` still gates the work so a
|
|
244
|
-
* since-renamed session is never overwritten.
|
|
245
|
-
*/
|
|
246
|
-
titleGenerationAttempted = new Set();
|
|
247
237
|
disposed = false;
|
|
248
238
|
constructor(opts) {
|
|
249
239
|
this.store = opts.store;
|
|
@@ -252,9 +242,13 @@ export class SessionStreamManager {
|
|
|
252
242
|
this.machineJwt = opts.machineJwt;
|
|
253
243
|
this.bridgeFactory = opts.bridgeFactory ?? DEFAULT_BRIDGE_FACTORY;
|
|
254
244
|
this.agentDir = opts.agentDir;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
245
|
+
// Wire the project observations store singleton so the memory extension's
|
|
246
|
+
// read_project_observations tool can query cross-session observations.
|
|
247
|
+
setProjectObsStore({
|
|
248
|
+
insertProjectObservations: (pid, sid, obs, ts) => this.store.insertProjectObservations(pid, sid, obs, ts),
|
|
249
|
+
searchProjectObservations: (pid, q, limit) => this.store.searchProjectObservations(pid, q, limit),
|
|
250
|
+
getProjectByCwd: (cwd) => this.store.getProjectByCwd(cwd),
|
|
251
|
+
});
|
|
258
252
|
}
|
|
259
253
|
/**
|
|
260
254
|
* Attach a subscriber to a session. Lazily creates the underlying pi
|
|
@@ -1161,6 +1155,29 @@ export class SessionStreamManager {
|
|
|
1161
1155
|
prunePersistedHistoryAfterCompaction(this.store, stream.sessionId, stream.bridge);
|
|
1162
1156
|
}
|
|
1163
1157
|
persistObservationalMemorySnapshot(this.store, stream.sessionId, stream.bridge);
|
|
1158
|
+
// After compaction the session context has been reduced; push updated
|
|
1159
|
+
// context-window stats to all subscribers so the frontend's context
|
|
1160
|
+
// bar refreshes immediately instead of waiting for the next turn.
|
|
1161
|
+
const ctx = stream.bridge.getContextUsage?.();
|
|
1162
|
+
if (ctx) {
|
|
1163
|
+
stream.contextWindowUsed = ctx.tokens;
|
|
1164
|
+
stream.contextWindowMax = ctx.contextWindow;
|
|
1165
|
+
this.broadcast(stream, {
|
|
1166
|
+
type: "token_usage",
|
|
1167
|
+
messageId: "",
|
|
1168
|
+
usage: {
|
|
1169
|
+
inputTokens: 0,
|
|
1170
|
+
outputTokens: 0,
|
|
1171
|
+
cacheReadTokens: 0,
|
|
1172
|
+
cacheWriteTokens: 0,
|
|
1173
|
+
totalTokens: 0,
|
|
1174
|
+
cost: null,
|
|
1175
|
+
creditsUsed: 0,
|
|
1176
|
+
},
|
|
1177
|
+
contextWindowUsed: ctx.tokens,
|
|
1178
|
+
contextWindowMax: ctx.contextWindow,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1164
1181
|
const tokensMsg = typeof event.tokensBefore === "number"
|
|
1165
1182
|
? ` (from ~${event.tokensBefore.toLocaleString()} tokens)`
|
|
1166
1183
|
: "";
|
|
@@ -1192,11 +1209,6 @@ export class SessionStreamManager {
|
|
|
1192
1209
|
stream.lastFlushedEventCount = 0;
|
|
1193
1210
|
const finishedTurn = stream.currentTurn;
|
|
1194
1211
|
stream.currentTurn = null;
|
|
1195
|
-
// Fire-and-forget auto-title generation. Runs only once per session
|
|
1196
|
-
// per server lifetime, only when the session is still wearing its
|
|
1197
|
-
// default title, and never blocks the user's stream (the user's
|
|
1198
|
-
// turn is already complete by the time this runs).
|
|
1199
|
-
this.maybeGenerateTitle(stream, finishedTurn);
|
|
1200
1212
|
// Autonomous iterative loop (Ralph Wiggum pattern).
|
|
1201
1213
|
// When loopActive is set, check for completion marker, then re-send
|
|
1202
1214
|
// the ORIGINAL prompt so the agent sees its prior changes and
|
|
@@ -1253,6 +1265,14 @@ export class SessionStreamManager {
|
|
|
1253
1265
|
stream.forkCompactSourceId = null; // one-shot
|
|
1254
1266
|
this.triggerForkCompact(stream);
|
|
1255
1267
|
}
|
|
1268
|
+
// Auto-dequeue: after the turn finishes and no loop iteration is
|
|
1269
|
+
// pending, check the persistent prompt queue. If there's a queued
|
|
1270
|
+
// prompt, start it immediately without broadcasting agent_end —
|
|
1271
|
+
// the frontend transitions seamlessly to the next turn.
|
|
1272
|
+
if (!stream.loopActive || !stream.loopOriginalPrompt) {
|
|
1273
|
+
if (this.maybeAutoDequeue(stream))
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1256
1276
|
}
|
|
1257
1277
|
else if (event.type === "error") {
|
|
1258
1278
|
// An error event arriving outside a turn (or bubbling out of one) —
|
|
@@ -1300,98 +1320,6 @@ export class SessionStreamManager {
|
|
|
1300
1320
|
console.error(`[spectral] error: batch-persist flush failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1301
1321
|
}
|
|
1302
1322
|
}
|
|
1303
|
-
/**
|
|
1304
|
-
* Auto-title the session if it's still wearing the default title and we
|
|
1305
|
-
* haven't already attempted generation in this process. Fire-and-forget
|
|
1306
|
-
* (errors are caught, logged, and swallowed) — the user's stream finished
|
|
1307
|
-
* before this runs, so blocking would only delay the broadcast.
|
|
1308
|
-
*/
|
|
1309
|
-
maybeGenerateTitle(stream, finishedTurn) {
|
|
1310
|
-
if (this.disableAutoTitle)
|
|
1311
|
-
return;
|
|
1312
|
-
if (this.titleGenerationAttempted.has(stream.sessionId))
|
|
1313
|
-
return;
|
|
1314
|
-
// Check the persisted title now (manual rename takes precedence).
|
|
1315
|
-
const detail = this.store.getSession(stream.sessionId);
|
|
1316
|
-
if (!detail || !isDefaultTitle(detail.title))
|
|
1317
|
-
return;
|
|
1318
|
-
// Find the first user message + the assistant text from the just-finished
|
|
1319
|
-
// turn. We deliberately read user content from SQLite (authoritative)
|
|
1320
|
-
// and assistant content from the in-memory turn buffer (cheap, and
|
|
1321
|
-
// matches what the user just saw).
|
|
1322
|
-
const firstUser = detail.messages.find((m) => m.role === "user");
|
|
1323
|
-
if (!firstUser || !firstUser.content.trim())
|
|
1324
|
-
return;
|
|
1325
|
-
let assistantText = finishedTurn?.assistantText ?? "";
|
|
1326
|
-
if (!assistantText) {
|
|
1327
|
-
// Fallback: pull the most recent assistant message from SQLite. This
|
|
1328
|
-
// can happen if `agent_end` fires for a turn whose buffer was cleared
|
|
1329
|
-
// by an intervening error event, or when this code path is reached
|
|
1330
|
-
// via a synthetic test event.
|
|
1331
|
-
const lastAssistant = [...detail.messages]
|
|
1332
|
-
.reverse()
|
|
1333
|
-
.find((m) => m.role === "assistant");
|
|
1334
|
-
assistantText = lastAssistant?.content ?? "";
|
|
1335
|
-
}
|
|
1336
|
-
// Mark BEFORE awaiting so a second `agent_end` arriving while we're
|
|
1337
|
-
// generating doesn't double-fire. Even if generation throws, we leave
|
|
1338
|
-
// the entry in place — one-shot semantics, no retries.
|
|
1339
|
-
this.titleGenerationAttempted.add(stream.sessionId);
|
|
1340
|
-
void this.runTitleGeneration(stream, firstUser.content, assistantText);
|
|
1341
|
-
}
|
|
1342
|
-
async runTitleGeneration(stream, firstUserMessage, firstAssistantMessage) {
|
|
1343
|
-
try {
|
|
1344
|
-
const title = await generateSessionTitle(firstUserMessage, firstAssistantMessage, {
|
|
1345
|
-
cwd: stream.cwd,
|
|
1346
|
-
agentDir: this.agentDir,
|
|
1347
|
-
llmCall: this.titleLlmCall,
|
|
1348
|
-
});
|
|
1349
|
-
if (this.disposed)
|
|
1350
|
-
return;
|
|
1351
|
-
if (!title)
|
|
1352
|
-
return;
|
|
1353
|
-
// Re-check the title hasn't been changed underneath us while we were
|
|
1354
|
-
// waiting on the LLM (e.g. user manually renamed mid-generation).
|
|
1355
|
-
const current = this.store.getSession(stream.sessionId);
|
|
1356
|
-
if (!current || !isDefaultTitle(current.title))
|
|
1357
|
-
return;
|
|
1358
|
-
const updated = this.store.renameSession(stream.sessionId, title);
|
|
1359
|
-
if (!updated)
|
|
1360
|
-
return;
|
|
1361
|
-
// Broadcast to every subscriber of this session so all open tabs
|
|
1362
|
-
// update their sidebar in real time. The wire event is independent
|
|
1363
|
-
// of the in-flight turn lifecycle, so it's safe to fire post-agent_end.
|
|
1364
|
-
this.broadcast(stream, {
|
|
1365
|
-
type: "session_renamed",
|
|
1366
|
-
sessionId: stream.sessionId,
|
|
1367
|
-
title: updated.title,
|
|
1368
|
-
});
|
|
1369
|
-
// Cross-tab fan-out hint: tabs that don't have THIS session open
|
|
1370
|
-
// (and so don't have a per-session ws subscription) still want to
|
|
1371
|
-
// refresh their sidebar to show the new title. Best-effort; a
|
|
1372
|
-
// failed publish never undoes the rename.
|
|
1373
|
-
if (this.publishMetaEvent) {
|
|
1374
|
-
try {
|
|
1375
|
-
this.publishMetaEvent({
|
|
1376
|
-
type: "session_renamed",
|
|
1377
|
-
projectId: updated.projectId,
|
|
1378
|
-
sessionId: stream.sessionId,
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
catch (err) {
|
|
1382
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1383
|
-
console.warn(`[spectral] warn: meta publish for auto-title failed: ${msg}`);
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
catch (err) {
|
|
1388
|
-
// Defensive: generateSessionTitle already swallows LLM errors. This
|
|
1389
|
-
// catches any unexpected throw from rename/broadcast so the manager
|
|
1390
|
-
// is never destabilized by a background title task.
|
|
1391
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1392
|
-
console.warn(`[spectral] warn: auto-title pipeline failed: ${msg}`);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
1323
|
broadcast(stream, event) {
|
|
1396
1324
|
const dead = [];
|
|
1397
1325
|
for (const sub of stream.subscribers) {
|
|
@@ -1410,6 +1338,43 @@ export class SessionStreamManager {
|
|
|
1410
1338
|
for (const sub of dead)
|
|
1411
1339
|
stream.subscribers.delete(sub);
|
|
1412
1340
|
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Push the current prompt queue state to all subscribers of a session.
|
|
1343
|
+
* Called after every queue mutation (enqueue, remove, clear, dequeue).
|
|
1344
|
+
* Safe to call when no stream exists or no subscribers — silently no-ops.
|
|
1345
|
+
*/
|
|
1346
|
+
pushQueueState(sessionId) {
|
|
1347
|
+
const stream = this.streams.get(sessionId);
|
|
1348
|
+
if (!stream || stream.subscribers.size === 0)
|
|
1349
|
+
return;
|
|
1350
|
+
const rows = this.store.getPromptQueue(sessionId);
|
|
1351
|
+
this.broadcast(stream, {
|
|
1352
|
+
type: "queue_changed",
|
|
1353
|
+
queue: rows.map((r) => ({
|
|
1354
|
+
id: r.id,
|
|
1355
|
+
content: r.content,
|
|
1356
|
+
position: r.position,
|
|
1357
|
+
createdAt: r.created_at,
|
|
1358
|
+
})),
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Auto-dequeue the next prompt from the persistent queue and start a
|
|
1363
|
+
* new turn. Called from the `agent_end` handler when no loop is active.
|
|
1364
|
+
* Returns true if a prompt was dequeued and a turn started.
|
|
1365
|
+
*/
|
|
1366
|
+
maybeAutoDequeue(stream) {
|
|
1367
|
+
const next = this.store.dequeuePrompt(stream.sessionId);
|
|
1368
|
+
if (!next)
|
|
1369
|
+
return false;
|
|
1370
|
+
// Push updated queue state so the frontend sees the item was removed.
|
|
1371
|
+
this.pushQueueState(stream.sessionId);
|
|
1372
|
+
// Fire the next prompt. Reuses the sticky model from the just-completed
|
|
1373
|
+
// turn (persisted by `prompt()` into the sessions.model_id column).
|
|
1374
|
+
const lastModelId = this.store.getSessionModel(stream.sessionId);
|
|
1375
|
+
void this.prompt(stream.sessionId, next.content, lastModelId ?? undefined);
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1413
1378
|
}
|
|
1414
1379
|
function isReplayable(event) {
|
|
1415
1380
|
return (event.type === "message_start" ||
|
package/dist/server/storage.js
CHANGED
|
@@ -36,7 +36,7 @@ import { stripJsoncComments } from "../studio-binding.js";
|
|
|
36
36
|
* Since this is local-only conversation history pre-1.0, we explicitly do not
|
|
37
37
|
* preserve user data across schema changes.
|
|
38
38
|
*/
|
|
39
|
-
const SCHEMA_VERSION =
|
|
39
|
+
const SCHEMA_VERSION = 4;
|
|
40
40
|
const SCHEMA_SQL = `
|
|
41
41
|
PRAGMA foreign_keys = ON;
|
|
42
42
|
|
|
@@ -78,6 +78,28 @@ CREATE TABLE IF NOT EXISTS session_memory_snapshots (
|
|
|
78
78
|
covered_source_count INTEGER NOT NULL,
|
|
79
79
|
updated_at INTEGER NOT NULL
|
|
80
80
|
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS project_observations (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
85
|
+
session_id TEXT NOT NULL,
|
|
86
|
+
content TEXT NOT NULL,
|
|
87
|
+
relevance TEXT NOT NULL CHECK (relevance IN ('low','medium','high','critical')),
|
|
88
|
+
created_at INTEGER NOT NULL
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_project_obs_project ON project_observations(project_id, created_at DESC);
|
|
92
|
+
|
|
93
|
+
CREATE TABLE IF NOT EXISTS prompt_queue (
|
|
94
|
+
id TEXT PRIMARY KEY,
|
|
95
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
96
|
+
content TEXT NOT NULL,
|
|
97
|
+
position INTEGER NOT NULL,
|
|
98
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
99
|
+
UNIQUE(session_id, position)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_prompt_queue_session ON prompt_queue(session_id, position);
|
|
81
103
|
`;
|
|
82
104
|
/**
|
|
83
105
|
* Synchronous binding-file reader for a project at the given filesystem path.
|
|
@@ -108,7 +130,7 @@ function applyBindingFields(project) {
|
|
|
108
130
|
};
|
|
109
131
|
}
|
|
110
132
|
/** Tables we own — used by the migration drop step. */
|
|
111
|
-
const KNOWN_TABLES = ["session_memory_snapshots", "messages", "sessions", "projects"];
|
|
133
|
+
const KNOWN_TABLES = ["prompt_queue", "project_observations", "session_memory_snapshots", "messages", "sessions", "projects"];
|
|
112
134
|
/** Best-effort parse of the `images_json` column into `ImageAttachment[]`. */
|
|
113
135
|
function parseImagesJson(raw) {
|
|
114
136
|
if (!raw || raw === "")
|
|
@@ -169,6 +191,17 @@ export class SessionStore {
|
|
|
169
191
|
// Fork & Compact: flag + per-session source tracking.
|
|
170
192
|
stmtSetForkCompactSource;
|
|
171
193
|
stmtGetForkCompactSource;
|
|
194
|
+
// Project observations: cross-session durable memory.
|
|
195
|
+
stmtInsertProjectObs;
|
|
196
|
+
stmtSearchProjectObs;
|
|
197
|
+
stmtGetProjectByCwd;
|
|
198
|
+
// Prompt queue statements
|
|
199
|
+
stmtEnqueuePrompt;
|
|
200
|
+
stmtGetPromptQueue;
|
|
201
|
+
stmtDequeuePrompt;
|
|
202
|
+
stmtDeleteQueueItem;
|
|
203
|
+
stmtClearSessionQueue;
|
|
204
|
+
stmtShiftPositions;
|
|
172
205
|
constructor(path) {
|
|
173
206
|
this.path = path;
|
|
174
207
|
// Make sure the parent directory exists. mkdirSync with recursive is a
|
|
@@ -307,6 +340,27 @@ export class SessionStore {
|
|
|
307
340
|
this.stmtSetSessionModel = this.db.prepare(`UPDATE sessions SET model_id = ? WHERE id = ?`);
|
|
308
341
|
this.stmtSetForkCompactSource = this.db.prepare(`UPDATE sessions SET fork_compact_source_id = ? WHERE id = ?`);
|
|
309
342
|
this.stmtGetForkCompactSource = this.db.prepare(`SELECT fork_compact_source_id FROM sessions WHERE id = ?`);
|
|
343
|
+
this.stmtInsertProjectObs = this.db.prepare(`INSERT OR REPLACE INTO project_observations (id, project_id, session_id, content, relevance, created_at)
|
|
344
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
345
|
+
this.stmtSearchProjectObs = this.db.prepare(`SELECT id, project_id, session_id, content, relevance, created_at
|
|
346
|
+
FROM project_observations
|
|
347
|
+
WHERE project_id = ? AND content LIKE ?
|
|
348
|
+
ORDER BY created_at DESC
|
|
349
|
+
LIMIT 20`);
|
|
350
|
+
this.stmtGetProjectByCwd = this.db.prepare(`SELECT id FROM projects WHERE path = ? LIMIT 1`);
|
|
351
|
+
// ---- prompt queue statements ----------------------------------------
|
|
352
|
+
this.stmtEnqueuePrompt = this.db.prepare(`INSERT INTO prompt_queue (id, session_id, content, position, created_at)
|
|
353
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
354
|
+
this.stmtGetPromptQueue = this.db.prepare(`SELECT id, session_id, content, position, created_at
|
|
355
|
+
FROM prompt_queue WHERE session_id = ?
|
|
356
|
+
ORDER BY position ASC`);
|
|
357
|
+
this.stmtDequeuePrompt = this.db.prepare(`SELECT id, session_id, content, position, created_at
|
|
358
|
+
FROM prompt_queue WHERE session_id = ?
|
|
359
|
+
ORDER BY position ASC LIMIT 1`);
|
|
360
|
+
this.stmtDeleteQueueItem = this.db.prepare(`DELETE FROM prompt_queue WHERE id = ? AND session_id = ?`);
|
|
361
|
+
this.stmtClearSessionQueue = this.db.prepare(`DELETE FROM prompt_queue WHERE session_id = ?`);
|
|
362
|
+
this.stmtShiftPositions = this.db.prepare(`UPDATE prompt_queue SET position = position + ?
|
|
363
|
+
WHERE session_id = ? AND position >= ?`);
|
|
310
364
|
}
|
|
311
365
|
/** Smoke check: returns the names of the tables in the DB. */
|
|
312
366
|
listTables() {
|
|
@@ -653,6 +707,104 @@ export class SessionStore {
|
|
|
653
707
|
clearForkCompactSource(sessionId) {
|
|
654
708
|
this.stmtSetForkCompactSource.run(null, sessionId);
|
|
655
709
|
}
|
|
710
|
+
// ----------------------------------------------------------------------
|
|
711
|
+
// Project observations (cross-session durable memory)
|
|
712
|
+
// ----------------------------------------------------------------------
|
|
713
|
+
/**
|
|
714
|
+
* Insert multiple project observations in a single transaction.
|
|
715
|
+
* Uses INSERT OR REPLACE so re-running after the same compaction is idempotent.
|
|
716
|
+
*/
|
|
717
|
+
insertProjectObservations(projectId, sessionId, observations, createdAt) {
|
|
718
|
+
if (observations.length === 0)
|
|
719
|
+
return;
|
|
720
|
+
const tx = this.db.transaction(() => {
|
|
721
|
+
for (const obs of observations) {
|
|
722
|
+
this.stmtInsertProjectObs.run(obs.id, projectId, sessionId, obs.content, obs.relevance, createdAt);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
tx();
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Search project observations by substring match. Returns up to 20 most recent.
|
|
729
|
+
*/
|
|
730
|
+
searchProjectObservations(projectId, query, limit = 20) {
|
|
731
|
+
const pattern = `%${query}%`;
|
|
732
|
+
const rows = this.stmtSearchProjectObs.all(projectId, pattern);
|
|
733
|
+
return rows.slice(0, limit).map((r) => ({
|
|
734
|
+
content: r.content,
|
|
735
|
+
relevance: r.relevance,
|
|
736
|
+
createdAt: r.created_at,
|
|
737
|
+
sessionId: r.session_id,
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Look up a project by its absolute filesystem path.
|
|
742
|
+
* Returns null when no project has been registered for the given path.
|
|
743
|
+
*/
|
|
744
|
+
getProjectByCwd(cwd) {
|
|
745
|
+
const row = this.stmtGetProjectByCwd.get(cwd);
|
|
746
|
+
return row?.id ?? null;
|
|
747
|
+
}
|
|
748
|
+
// ----------------------------------------------------------------------
|
|
749
|
+
// Prompt Queue
|
|
750
|
+
// ----------------------------------------------------------------------
|
|
751
|
+
/**
|
|
752
|
+
* Enqueue a prompt for a session. Returns the created queue item.
|
|
753
|
+
* Position is auto-assigned as (max existing position + 1).
|
|
754
|
+
*/
|
|
755
|
+
enqueuePrompt(sessionId, content) {
|
|
756
|
+
const id = randomUUID();
|
|
757
|
+
const items = this.stmtGetPromptQueue.all(sessionId);
|
|
758
|
+
const nextPosition = items.length > 0
|
|
759
|
+
? Math.max(...items.map((r) => r.position)) + 1
|
|
760
|
+
: 0;
|
|
761
|
+
const createdAt = new Date().toISOString();
|
|
762
|
+
this.stmtEnqueuePrompt.run(id, sessionId, content, nextPosition, createdAt);
|
|
763
|
+
return { id, session_id: sessionId, content, position: nextPosition, created_at: createdAt };
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Dequeue (remove and return) the first prompt for a session.
|
|
767
|
+
* Returns null if the queue is empty.
|
|
768
|
+
* Renumbers remaining items so positions stay contiguous from 0.
|
|
769
|
+
*/
|
|
770
|
+
dequeuePrompt(sessionId) {
|
|
771
|
+
const row = this.stmtDequeuePrompt.get(sessionId);
|
|
772
|
+
if (!row)
|
|
773
|
+
return null;
|
|
774
|
+
const tx = this.db.transaction(() => {
|
|
775
|
+
this.stmtDeleteQueueItem.run(row.id, sessionId);
|
|
776
|
+
// Shift remaining positions down by 1
|
|
777
|
+
this.stmtShiftPositions.run(sessionId, -1, row.position);
|
|
778
|
+
});
|
|
779
|
+
tx();
|
|
780
|
+
return row;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Get the full prompt queue for a session, ordered by position.
|
|
784
|
+
*/
|
|
785
|
+
getPromptQueue(sessionId) {
|
|
786
|
+
return this.stmtGetPromptQueue.all(sessionId);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Remove a specific prompt from the queue by id.
|
|
790
|
+
* Renumbers remaining items so positions stay contiguous.
|
|
791
|
+
*/
|
|
792
|
+
removePrompt(sessionId, itemId) {
|
|
793
|
+
const row = this.stmtGetPromptQueue.all(sessionId).find((r) => r.id === itemId);
|
|
794
|
+
if (!row)
|
|
795
|
+
return;
|
|
796
|
+
const tx = this.db.transaction(() => {
|
|
797
|
+
this.stmtDeleteQueueItem.run(itemId, sessionId);
|
|
798
|
+
this.stmtShiftPositions.run(sessionId, -1, row.position);
|
|
799
|
+
});
|
|
800
|
+
tx();
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Clear the entire prompt queue for a session.
|
|
804
|
+
*/
|
|
805
|
+
clearPromptQueue(sessionId) {
|
|
806
|
+
this.stmtClearSessionQueue.run(sessionId);
|
|
807
|
+
}
|
|
656
808
|
close() {
|
|
657
809
|
if (this.closed)
|
|
658
810
|
return;
|