@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.4
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/AGENTS.md +8 -2
- package/ARCHITECTURE.md +2 -0
- package/docs/architecture/memory.md +15 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +11 -6
- package/docs/guardian-request-flow.md +35 -0
- package/knip.json +3 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/openapi.yaml +136 -76
- package/package.json +1 -1
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/conversation-agent-loop.test.ts +4 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +39 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +53 -0
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/file-ops-service.test.ts +163 -30
- package/src/__tests__/filesystem-tools.test.ts +23 -24
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/host-file-read-tool.test.ts +16 -19
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
- package/src/__tests__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/tool-executor.test.ts +5 -1
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/acp/session-manager.ts +8 -1
- package/src/api/events/host-file.ts +2 -2
- package/src/api/surfaces.ts +5 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
- package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
- package/src/calls/voice-session-bridge.ts +44 -25
- package/src/calls/voice-triage-escalate.ts +1 -0
- package/src/cli/bundled-modules.ts +29 -0
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
- package/src/config/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- package/src/config/call-site-defaults.ts +7 -0
- package/src/config/default-profile-catalog.ts +96 -4
- package/src/config/feature-flag-registry.json +11 -11
- package/src/config/skills.ts +9 -2
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +14 -13
- package/src/daemon/conversation-notifiers.ts +11 -9
- package/src/daemon/conversation-process.ts +0 -27
- package/src/daemon/conversation-runtime-assembly.ts +9 -2
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +27 -13
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +104 -51
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -13
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +5 -235
- package/src/hooks/types.ts +5 -0
- package/src/ipc/gateway-flag-listener.ts +17 -3
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
- package/src/live-voice/live-voice-manager.ts +16 -3
- package/src/live-voice/live-voice-session.ts +63 -9
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- package/src/monitoring/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/approval-card-data.ts +33 -0
- package/src/permissions/prompter.ts +1 -5
- package/src/persistence/conversation-queries.ts +66 -16
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
- package/src/persistence/schema/documents.ts +4 -4
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/index.ts +5 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/defaults/main.ts +6 -7
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/mtime-cache.ts +17 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- package/src/providers/__tests__/retry-callsite.test.ts +20 -0
- package/src/providers/openai/chat-completions-provider.ts +11 -1
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
- package/src/routes/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/agent-wake.ts +15 -12
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/conversation-list-routes.ts +54 -22
- package/src/runtime/routes/conversation-management-routes.ts +2 -3
- package/src/runtime/routes/conversation-routes.ts +11 -13
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +20 -37
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +15 -13
- package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
- package/src/tools/acp/spawn.ts +6 -4
- package/src/tools/filesystem/read.ts +27 -10
- package/src/tools/host-filesystem/read.ts +27 -15
- package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
- package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
- package/src/tools/shared/filesystem/types.ts +5 -5
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/daemon/interactive-turn-sender.ts +0 -59
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schedule list and detail routes say why a plugin-sourced schedule is
|
|
3
|
+
* off, so a client never has to show an unexplained disabled row.
|
|
4
|
+
*
|
|
5
|
+
* The reason is derived from the row plus the plugin's files, so the fixtures
|
|
6
|
+
* here write real plugin directories under the per-test-process workspace
|
|
7
|
+
* plugins dir and converge them against the real schedule store.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
import { setOverridesForTesting } from "../../../__tests__/feature-flag-test-helpers.js";
|
|
15
|
+
import { getDb } from "../../../persistence/db-connection.js";
|
|
16
|
+
import { initializeDb } from "../../../persistence/db-init.js";
|
|
17
|
+
import {
|
|
18
|
+
createSchedule,
|
|
19
|
+
setUserEnabled,
|
|
20
|
+
upsertDeclaredSchedule,
|
|
21
|
+
} from "../../../schedule/schedule-store.js";
|
|
22
|
+
import { getWorkspacePluginsDir } from "../../../util/platform.js";
|
|
23
|
+
import { ROUTES } from "../schedule-routes.js";
|
|
24
|
+
|
|
25
|
+
await initializeDb();
|
|
26
|
+
|
|
27
|
+
const pluginsDir = getWorkspacePluginsDir();
|
|
28
|
+
|
|
29
|
+
function routeHandler(operationId: string) {
|
|
30
|
+
const route = ROUTES.find((r) => r.operationId === operationId);
|
|
31
|
+
if (!route) {
|
|
32
|
+
throw new Error(`${operationId} route not found`);
|
|
33
|
+
}
|
|
34
|
+
return route.handler;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const listHandler = routeHandler("listSchedules");
|
|
38
|
+
const getHandler = routeHandler("getSchedule");
|
|
39
|
+
|
|
40
|
+
interface SerializedSchedule {
|
|
41
|
+
id: string;
|
|
42
|
+
sourceKey: string | null;
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
disarmReason: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function listed(id: string): Promise<SerializedSchedule> {
|
|
48
|
+
const { schedules } = (await listHandler({ queryParams: {} })) as {
|
|
49
|
+
schedules: SerializedSchedule[];
|
|
50
|
+
};
|
|
51
|
+
const found = schedules.find((s) => s.id === id);
|
|
52
|
+
if (!found) {
|
|
53
|
+
throw new Error(`schedule ${id} missing from the list response`);
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function fetched(id: string): Promise<SerializedSchedule> {
|
|
59
|
+
const { schedule } = (await getHandler({ pathParams: { id } })) as {
|
|
60
|
+
schedule: SerializedSchedule;
|
|
61
|
+
};
|
|
62
|
+
return schedule;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Write a plugin dir holding one `schedules/<name>/` declaration. */
|
|
66
|
+
function writePlugin(
|
|
67
|
+
pluginName: string,
|
|
68
|
+
options: { declaration?: string; disabled?: boolean } = {},
|
|
69
|
+
): void {
|
|
70
|
+
const dir = join(pluginsDir, pluginName);
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
writeFileSync(
|
|
73
|
+
join(dir, "package.json"),
|
|
74
|
+
JSON.stringify({ name: pluginName, version: "1.0.0" }),
|
|
75
|
+
);
|
|
76
|
+
if (options.declaration) {
|
|
77
|
+
const declarationDir = join(dir, "schedules", options.declaration);
|
|
78
|
+
mkdirSync(declarationDir, { recursive: true });
|
|
79
|
+
writeFileSync(
|
|
80
|
+
join(declarationDir, "config.json"),
|
|
81
|
+
JSON.stringify({ expression: "* * * * *" }),
|
|
82
|
+
);
|
|
83
|
+
writeFileSync(join(declarationDir, "index.md"), "Summarize the day.\n");
|
|
84
|
+
}
|
|
85
|
+
if (options.disabled) {
|
|
86
|
+
writeFileSync(join(dir, ".disabled"), "");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A declared row for `plugin:<pluginName>/digest`, armed or not. */
|
|
91
|
+
async function declaredRow(
|
|
92
|
+
pluginName: string,
|
|
93
|
+
enabled: boolean,
|
|
94
|
+
): Promise<string> {
|
|
95
|
+
const job = await upsertDeclaredSchedule(`plugin:${pluginName}/digest`, {
|
|
96
|
+
name: `${pluginName}-digest`,
|
|
97
|
+
description: "Daily digest",
|
|
98
|
+
syntax: "cron",
|
|
99
|
+
expression: "* * * * *",
|
|
100
|
+
message: "Summarize the day.",
|
|
101
|
+
mode: "execute",
|
|
102
|
+
enabled,
|
|
103
|
+
definitionHash: `hash-${pluginName}`,
|
|
104
|
+
});
|
|
105
|
+
return job.id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Stamp the engine's exhaust latch onto a row: off, spent, and last run. */
|
|
109
|
+
function latchAsCompleted(id: string): void {
|
|
110
|
+
getDb().run(
|
|
111
|
+
`UPDATE cron_jobs SET enabled = 0, next_run_at = 0, last_run_at = ${Date.now()} WHERE id = '${id}'`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
getDb().run("DELETE FROM cron_runs");
|
|
117
|
+
getDb().run("DELETE FROM cron_jobs");
|
|
118
|
+
rmSync(pluginsDir, { recursive: true, force: true });
|
|
119
|
+
// The feature ships off, so every case states the flag it runs under rather
|
|
120
|
+
// than inheriting the registry default.
|
|
121
|
+
setOverridesForTesting({ "plugin-schedules": true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("schedule serialization: disarmReason", () => {
|
|
125
|
+
test("a user's own override outranks whatever the plugin's files say", async () => {
|
|
126
|
+
writePlugin("news", { declaration: "digest" });
|
|
127
|
+
const id = await declaredRow("news", true);
|
|
128
|
+
await setUserEnabled(id, false);
|
|
129
|
+
|
|
130
|
+
expect((await listed(id)).disarmReason).toBe("user_disabled");
|
|
131
|
+
expect((await fetched(id)).disarmReason).toBe("user_disabled");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a plugin that is gone from disk reads as removed", async () => {
|
|
135
|
+
const id = await declaredRow("news", false);
|
|
136
|
+
|
|
137
|
+
expect((await listed(id)).disarmReason).toBe("plugin_removed");
|
|
138
|
+
expect((await fetched(id)).disarmReason).toBe("plugin_removed");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a plugin carrying the .disabled sentinel reads as disabled", async () => {
|
|
142
|
+
writePlugin("news", { declaration: "digest", disabled: true });
|
|
143
|
+
const id = await declaredRow("news", false);
|
|
144
|
+
|
|
145
|
+
expect((await listed(id)).disarmReason).toBe("plugin_disabled");
|
|
146
|
+
expect((await fetched(id)).disarmReason).toBe("plugin_disabled");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a declaration dropped from a plugin that is still installed", async () => {
|
|
150
|
+
writePlugin("news");
|
|
151
|
+
const id = await declaredRow("news", false);
|
|
152
|
+
|
|
153
|
+
expect((await listed(id)).disarmReason).toBe("declaration_removed");
|
|
154
|
+
expect((await fetched(id)).disarmReason).toBe("declaration_removed");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("a declaration still on disk means the plugin's files turned it off", async () => {
|
|
158
|
+
writePlugin("news", { declaration: "digest" });
|
|
159
|
+
const id = await declaredRow("news", false);
|
|
160
|
+
|
|
161
|
+
expect((await listed(id)).disarmReason).toBe("declaration_disabled");
|
|
162
|
+
expect((await fetched(id)).disarmReason).toBe("declaration_disabled");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("an armed plugin row has no reason to give", async () => {
|
|
166
|
+
writePlugin("news", { declaration: "digest" });
|
|
167
|
+
const id = await declaredRow("news", true);
|
|
168
|
+
|
|
169
|
+
const row = await listed(id);
|
|
170
|
+
expect(row.enabled).toBe(true);
|
|
171
|
+
expect(row.disarmReason).toBeNull();
|
|
172
|
+
expect((await fetched(id)).disarmReason).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("a run-to-completion row reads as finished, not paused", async () => {
|
|
176
|
+
writePlugin("news", { declaration: "digest" });
|
|
177
|
+
const id = await declaredRow("news", true);
|
|
178
|
+
latchAsCompleted(id);
|
|
179
|
+
|
|
180
|
+
const row = await listed(id);
|
|
181
|
+
expect(row.enabled).toBe(false);
|
|
182
|
+
expect(row.disarmReason).toBeNull();
|
|
183
|
+
expect((await fetched(id)).disarmReason).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("the kill switch being off is not a per-row cause", async () => {
|
|
187
|
+
writePlugin("news", { declaration: "digest" });
|
|
188
|
+
const id = await declaredRow("news", false);
|
|
189
|
+
setOverridesForTesting({ "plugin-schedules": false });
|
|
190
|
+
|
|
191
|
+
const row = await listed(id);
|
|
192
|
+
expect(row.enabled).toBe(false);
|
|
193
|
+
expect(row.disarmReason).toBeNull();
|
|
194
|
+
expect((await fetched(id)).disarmReason).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a user-created schedule never carries one, off or on", async () => {
|
|
198
|
+
const job = await createSchedule({
|
|
199
|
+
name: "wt-imperative",
|
|
200
|
+
description: "d",
|
|
201
|
+
message: "do the thing",
|
|
202
|
+
mode: "execute",
|
|
203
|
+
enabled: false,
|
|
204
|
+
timezone: null,
|
|
205
|
+
expression: "* * * * *",
|
|
206
|
+
syntax: "cron",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const row = await listed(job.id);
|
|
210
|
+
expect(row.sourceKey).toBeNull();
|
|
211
|
+
expect(row.enabled).toBe(false);
|
|
212
|
+
expect(row.disarmReason).toBeNull();
|
|
213
|
+
expect((await fetched(job.id)).disarmReason).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -209,28 +209,41 @@ function resolveOrThrow(rawId: string): string {
|
|
|
209
209
|
// Handlers
|
|
210
210
|
// ---------------------------------------------------------------------------
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Read a query parameter that admits a closed set of values. Absent or empty
|
|
214
|
+
* reads as `undefined`; anything outside `accepted` is a 400. The strictness
|
|
215
|
+
* is the point: silently coercing a typo or a newer client's value to "no
|
|
216
|
+
* filter" would hand back the full list where a subset was asked for, and
|
|
217
|
+
* that skew is invisible to the client (it masks version skew too).
|
|
218
|
+
*/
|
|
219
|
+
function parseEnumQueryParam<const T extends readonly string[]>(
|
|
220
|
+
queryParams: Record<string, string | undefined>,
|
|
221
|
+
name: string,
|
|
222
|
+
accepted: T,
|
|
223
|
+
): T[number] | undefined {
|
|
224
|
+
const raw = queryParams[name];
|
|
225
|
+
if (raw === undefined || raw === "") {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
if ((accepted as readonly string[]).includes(raw)) {
|
|
229
|
+
return raw as T[number];
|
|
230
|
+
}
|
|
231
|
+
throw new BadRequestError(
|
|
232
|
+
`Unknown ${name} "${raw}"; expected ${accepted.map((v) => `"${v}"`).join(" or ")}.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
212
236
|
function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
|
|
213
237
|
const limit = Number(queryParams.limit ?? 50);
|
|
214
238
|
const offset = Number(queryParams.offset ?? 0);
|
|
215
239
|
// "background" is the back-compat umbrella (background + scheduled); newer
|
|
216
240
|
// clients can pass "scheduled" to load only the Scheduled section. Absent
|
|
217
|
-
// defaults to the standard foreground list.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
rawConversationType === "background" ||
|
|
225
|
-
rawConversationType === "scheduled"
|
|
226
|
-
) {
|
|
227
|
-
conversationType = rawConversationType;
|
|
228
|
-
} else {
|
|
229
|
-
throw new BadRequestError(
|
|
230
|
-
`Unknown conversationType "${rawConversationType}"; expected "background" or "scheduled".`,
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
241
|
+
// defaults to the standard foreground list.
|
|
242
|
+
const conversationType: ConversationType =
|
|
243
|
+
parseEnumQueryParam(queryParams, "conversationType", [
|
|
244
|
+
"background",
|
|
245
|
+
"scheduled",
|
|
246
|
+
]) ?? "standard";
|
|
234
247
|
// Defaults to `active` so sidebar restores no longer pull archived rows.
|
|
235
248
|
// The Archive page opts into `archived` to render only archived rows
|
|
236
249
|
// without dragging the entire live history through pagination first.
|
|
@@ -251,11 +264,17 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
|
|
|
251
264
|
? queryParams.groupId
|
|
252
265
|
: undefined;
|
|
253
266
|
|
|
267
|
+
const needsAttention =
|
|
268
|
+
parseEnumQueryParam(queryParams, "needsAttention", ["true"]) === "true"
|
|
269
|
+
? true
|
|
270
|
+
: undefined;
|
|
271
|
+
|
|
254
272
|
const filter: ConversationListFilter = {
|
|
255
273
|
conversationType,
|
|
256
274
|
archiveStatus,
|
|
257
275
|
originChannel,
|
|
258
276
|
groupId,
|
|
277
|
+
needsAttention,
|
|
259
278
|
};
|
|
260
279
|
let rows = listConversations({ ...filter, limit, offset });
|
|
261
280
|
const totalCount = countConversations(filter);
|
|
@@ -270,15 +289,19 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
|
|
|
270
289
|
// Skipped for group-scoped queries for the same reason: a caller asking
|
|
271
290
|
// for one group gets that group, and a client that fetches the Pinned
|
|
272
291
|
// section via `groupId=system:pinned` has no use for rows appended to
|
|
273
|
-
// some other group's page.
|
|
274
|
-
//
|
|
275
|
-
//
|
|
292
|
+
// some other group's page. Skipped for attention-scoped queries too: the
|
|
293
|
+
// caller asked for the unseen subset, and a seen pinned row appended to
|
|
294
|
+
// it would put a row outside the filter in the page while `hasMore` is
|
|
295
|
+
// computed from the filtered count. This is the compatibility shim for
|
|
296
|
+
// clients that still read Pinned out of the unfiltered list; it goes
|
|
297
|
+
// away once every section fetches its own group.
|
|
276
298
|
if (
|
|
277
299
|
offset === 0 &&
|
|
278
300
|
conversationType === "standard" &&
|
|
279
301
|
archiveStatus === "active" &&
|
|
280
302
|
originChannel === undefined &&
|
|
281
|
-
groupId === undefined
|
|
303
|
+
groupId === undefined &&
|
|
304
|
+
needsAttention === undefined
|
|
282
305
|
) {
|
|
283
306
|
const pinned = listPinnedConversations(archiveStatus);
|
|
284
307
|
const seen = new Set(rows.map((c) => c.id));
|
|
@@ -580,6 +603,14 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
580
603
|
description:
|
|
581
604
|
'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is recency-ordered like every list read (COALESCE(last_message_at, updated_at) descending) and never has pinned rows appended to it. Omit to span every group.',
|
|
582
605
|
},
|
|
606
|
+
{
|
|
607
|
+
name: "needsAttention",
|
|
608
|
+
type: "string",
|
|
609
|
+
required: false,
|
|
610
|
+
description:
|
|
611
|
+
'Pass "true" to return only conversations whose latest assistant message the user has not seen: the same predicate behind the unread count and the section index, so a client that keeps no complete conversation list can ask for exactly the rows its attention surfaces need. Composes with every other filter. Any value other than "true" is rejected. Omit to span every conversation.',
|
|
612
|
+
schema: { type: "string", enum: ["true"] },
|
|
613
|
+
},
|
|
583
614
|
],
|
|
584
615
|
responseBody: listConversationsResponseSchema,
|
|
585
616
|
handler: handleListConversations,
|
|
@@ -683,7 +714,8 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
683
714
|
},
|
|
684
715
|
pathParams: [{ name: "id", type: "uuid" }],
|
|
685
716
|
summary: "Get conversation detail",
|
|
686
|
-
description:
|
|
717
|
+
description:
|
|
718
|
+
"Retrieve a single conversation with full metadata. Rows the listing hides by type (legacy private rows) are not found here either.",
|
|
687
719
|
tags: ["conversations"],
|
|
688
720
|
responseBody: conversationDetailResponseSchema,
|
|
689
721
|
handler: handleGetConversation,
|
|
@@ -308,9 +308,8 @@ async function handleSummarizeConversation({ body = {} }: RouteHandlerArgs) {
|
|
|
308
308
|
statusText: "Summarizing conversation",
|
|
309
309
|
});
|
|
310
310
|
// The context-window usage push goes out on the same broadcast path as
|
|
311
|
-
// the result card below
|
|
312
|
-
//
|
|
313
|
-
// sender and a `sendToClient` emit would reach nobody.
|
|
311
|
+
// the result card below, so the two can never be delivered to different
|
|
312
|
+
// places.
|
|
314
313
|
const result = await conversation.summarizeUpToMessage(
|
|
315
314
|
beforeMessageId,
|
|
316
315
|
broadcastMessage,
|
|
@@ -1979,20 +1979,18 @@ export async function handleSendMessage(
|
|
|
1979
1979
|
sourceActorPrincipalId,
|
|
1980
1980
|
);
|
|
1981
1981
|
}
|
|
1982
|
-
//
|
|
1983
|
-
//
|
|
1984
|
-
//
|
|
1985
|
-
//
|
|
1986
|
-
//
|
|
1987
|
-
//
|
|
1988
|
-
//
|
|
1989
|
-
// proxy explicitly below without relaxing `hasNoClient`.
|
|
1990
|
-
conversation.updateClient(broadcastMessage, !isInteractive);
|
|
1982
|
+
// Delivery needs no wiring: the conversation's sink is the SSE hub for its
|
|
1983
|
+
// whole life. Presence travels with the turn (`isInteractive` below), which
|
|
1984
|
+
// is what keeps host_bash/host_file/host_cu gated for non-desktop
|
|
1985
|
+
// interfaces. The chrome-extension interface is non-interactive (no SSE
|
|
1986
|
+
// prompter UI) but still has a connected client that can service
|
|
1987
|
+
// host_browser_request events; that single proxy is restored explicitly
|
|
1988
|
+
// below without claiming a human is present.
|
|
1991
1989
|
if (isInteractive) {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
);
|
|
1990
|
+
// A client that reconnected mid-phase resyncs the current activity state
|
|
1991
|
+
// and any subagent card it left stale while disconnected.
|
|
1992
|
+
conversation.replayActivityState();
|
|
1993
|
+
getSubagentManager().reannounceChildStatuses(mapping.conversationId);
|
|
1996
1994
|
}
|
|
1997
1995
|
|
|
1998
1996
|
// ── URL scan path: rewrite first message for scan onboarding ──
|
|
@@ -4,20 +4,12 @@
|
|
|
4
4
|
* Exposes document CRUD over HTTP, sharing business logic with the
|
|
5
5
|
* handlers in `daemon/handlers/documents.ts`.
|
|
6
6
|
*/
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { readFileSync, statSync } from "node:fs";
|
|
9
|
-
import { basename, extname } from "node:path";
|
|
10
|
-
|
|
11
7
|
import { z } from "zod";
|
|
12
8
|
|
|
13
9
|
import {
|
|
14
10
|
addDocumentConversation,
|
|
15
|
-
createFileBackedDocument,
|
|
16
11
|
getDocumentById,
|
|
17
|
-
getDocumentByWorkspacePath,
|
|
18
12
|
getDocumentsForConversation,
|
|
19
|
-
isDocumentAssociatedWithConversation,
|
|
20
|
-
refreshDocumentContentFromFile,
|
|
21
13
|
saveDocument,
|
|
22
14
|
} from "../../documents/document-store.js";
|
|
23
15
|
import { rawAll } from "../../persistence/raw-query.js";
|
|
@@ -28,18 +20,9 @@ import {
|
|
|
28
20
|
publishDocumentsChanged,
|
|
29
21
|
} from "../sync/resource-sync-events.js";
|
|
30
22
|
import { renderMarkdownToPDF } from "./document-pdf-renderer.js";
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
InternalError,
|
|
34
|
-
NotFoundError,
|
|
35
|
-
UnprocessableEntityError,
|
|
36
|
-
} from "./errors.js";
|
|
37
|
-
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
23
|
+
import { BadRequestError, InternalError, NotFoundError } from "./errors.js";
|
|
24
|
+
import type { RouteDefinition } from "./types.js";
|
|
38
25
|
import { RouteResponse } from "./types.js";
|
|
39
|
-
import {
|
|
40
|
-
resolveWorkspacePath,
|
|
41
|
-
toWorkspaceRelativePath,
|
|
42
|
-
} from "./workspace-utils.js";
|
|
43
26
|
|
|
44
27
|
const log = getLogger("documents-routes");
|
|
45
28
|
|
|
@@ -85,14 +68,7 @@ function listAllDocuments(): Array<{
|
|
|
85
68
|
}
|
|
86
69
|
}
|
|
87
70
|
|
|
88
|
-
|
|
89
|
-
// File-backed documents
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
/** Extensions the document editor round-trips faithfully as markdown. */
|
|
93
|
-
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown"]);
|
|
94
|
-
|
|
95
|
-
/** The document payload shape shared by `GET documents/{id}` and its siblings. */
|
|
71
|
+
/** The document payload shape returned by `GET documents/{id}`. */
|
|
96
72
|
const documentPayloadSchema = z.object({
|
|
97
73
|
success: z.boolean(),
|
|
98
74
|
surfaceId: z.string(),
|
|
@@ -102,173 +78,8 @@ const documentPayloadSchema = z.object({
|
|
|
102
78
|
wordCount: z.number(),
|
|
103
79
|
createdAt: z.number(),
|
|
104
80
|
updatedAt: z.number(),
|
|
105
|
-
workspacePath: z
|
|
106
|
-
.string()
|
|
107
|
-
.nullable()
|
|
108
|
-
.describe("Workspace-relative file this document is bound to, or null"),
|
|
109
81
|
});
|
|
110
82
|
|
|
111
|
-
/**
|
|
112
|
-
* Resolve a client-supplied workspace path to the markdown file it names.
|
|
113
|
-
*
|
|
114
|
-
* Uses the same resolution as the workspace file routes, so a path that
|
|
115
|
-
* escapes the workspace (traversal, absolute, symlinked out, dot-segment) is
|
|
116
|
-
* rejected here exactly as it would be there.
|
|
117
|
-
*/
|
|
118
|
-
function resolveWorkspaceMarkdownFile(path: string): {
|
|
119
|
-
absolutePath: string;
|
|
120
|
-
relativePath: string;
|
|
121
|
-
} {
|
|
122
|
-
const resolved = resolveWorkspacePath(path);
|
|
123
|
-
if (resolved === undefined) {
|
|
124
|
-
throw new BadRequestError("Invalid path");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!MARKDOWN_EXTENSIONS.has(extname(resolved).toLowerCase())) {
|
|
128
|
-
throw new UnprocessableEntityError(
|
|
129
|
-
"Only markdown files (.md, .markdown) can back a document",
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
absolutePath: resolved,
|
|
135
|
-
relativePath: toWorkspaceRelativePath(resolved),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Current text of a workspace file, or `null` when it is gone or not a file. */
|
|
140
|
-
function readWorkspaceFileText(absolutePath: string): string | null {
|
|
141
|
-
try {
|
|
142
|
-
if (!statSync(absolutePath).isFile()) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
return readFileSync(absolutePath, "utf-8");
|
|
146
|
-
} catch {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Load a document by surface ID for a response, or fail loudly if it vanished. */
|
|
152
|
-
function loadDocumentPayload(surfaceId: string): Record<string, unknown> {
|
|
153
|
-
const doc = getDocumentById(surfaceId);
|
|
154
|
-
if (!doc) {
|
|
155
|
-
throw new InternalError("Document could not be loaded after persisting");
|
|
156
|
-
}
|
|
157
|
-
return { success: true, ...doc };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Find or create the document bound to a workspace markdown file.
|
|
162
|
-
*
|
|
163
|
-
* The file is the source of truth at open time: when a bound document's stored
|
|
164
|
-
* content has drifted from the bytes on disk (the file was edited outside the
|
|
165
|
-
* editor), the row is refreshed from the file before it is returned. Writes go
|
|
166
|
-
* the other way — the document store writes through to the file.
|
|
167
|
-
*
|
|
168
|
-
* The publishes here carry no origin client id on purpose. The only caller,
|
|
169
|
-
* `viewer-store.loadWorkspaceFileDocument`, opens the file and never
|
|
170
|
-
* invalidates its own document queries, so the client that creates the row is
|
|
171
|
-
* the one that most needs to hear about it: suppressing its own echo would
|
|
172
|
-
* leave its assets pill and Library without the document it just opened.
|
|
173
|
-
*/
|
|
174
|
-
function handleDocumentForWorkspaceFile({
|
|
175
|
-
body,
|
|
176
|
-
}: RouteHandlerArgs): Record<string, unknown> {
|
|
177
|
-
const { path, conversationId } = (body ?? {}) as {
|
|
178
|
-
path?: string;
|
|
179
|
-
conversationId?: string;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
if (!path || typeof path !== "string") {
|
|
183
|
-
throw new BadRequestError("path is required");
|
|
184
|
-
}
|
|
185
|
-
if (!conversationId || typeof conversationId !== "string") {
|
|
186
|
-
throw new BadRequestError("conversationId is required");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const { absolutePath, relativePath } = resolveWorkspaceMarkdownFile(path);
|
|
190
|
-
const existing = getDocumentByWorkspacePath(relativePath);
|
|
191
|
-
const fileText = readWorkspaceFileText(absolutePath);
|
|
192
|
-
|
|
193
|
-
if (fileText === null) {
|
|
194
|
-
// A bound document whose file is gone is reported distinctly: the client
|
|
195
|
-
// needs to tell "this file was deleted" apart from "no such file", and the
|
|
196
|
-
// document must never be used to resurrect the file.
|
|
197
|
-
if (existing) {
|
|
198
|
-
throw new NotFoundError(
|
|
199
|
-
`The file backing this document no longer exists: ${relativePath}`,
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
throw new NotFoundError("File not found");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (existing) {
|
|
206
|
-
// Opening the file from another conversation grants that conversation
|
|
207
|
-
// access, the same association the link route writes.
|
|
208
|
-
const alreadyLinked = isDocumentAssociatedWithConversation(
|
|
209
|
-
existing.surfaceId,
|
|
210
|
-
conversationId,
|
|
211
|
-
);
|
|
212
|
-
addDocumentConversation(existing.surfaceId, conversationId);
|
|
213
|
-
|
|
214
|
-
let refreshed = false;
|
|
215
|
-
if (existing.content !== fileText) {
|
|
216
|
-
const refresh = refreshDocumentContentFromFile(
|
|
217
|
-
existing.surfaceId,
|
|
218
|
-
fileText,
|
|
219
|
-
);
|
|
220
|
-
if (!refresh.success) {
|
|
221
|
-
throw new InternalError(refresh.error);
|
|
222
|
-
}
|
|
223
|
-
refreshed = true;
|
|
224
|
-
log.info(
|
|
225
|
-
{ surfaceId: existing.surfaceId, workspacePath: relativePath },
|
|
226
|
-
"Refreshed file-backed document from disk",
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Reopening an unchanged, already-linked file writes nothing, so it must
|
|
231
|
-
// not invalidate every client's document list on each open.
|
|
232
|
-
if (!alreadyLinked || refreshed) {
|
|
233
|
-
publishDocumentsChanged();
|
|
234
|
-
}
|
|
235
|
-
return loadDocumentPayload(existing.surfaceId);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const surfaceId = `doc-${randomUUID()}`;
|
|
239
|
-
const created = createFileBackedDocument({
|
|
240
|
-
surfaceId,
|
|
241
|
-
conversationId,
|
|
242
|
-
title: basename(relativePath),
|
|
243
|
-
content: fileText,
|
|
244
|
-
workspacePath: relativePath,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
if (!created.success) {
|
|
248
|
-
// A concurrent request may have won the unique index on workspace_path.
|
|
249
|
-
const raced = getDocumentByWorkspacePath(relativePath);
|
|
250
|
-
if (!raced) {
|
|
251
|
-
throw new InternalError(created.error);
|
|
252
|
-
}
|
|
253
|
-
const racedAlreadyLinked = isDocumentAssociatedWithConversation(
|
|
254
|
-
raced.surfaceId,
|
|
255
|
-
conversationId,
|
|
256
|
-
);
|
|
257
|
-
addDocumentConversation(raced.surfaceId, conversationId);
|
|
258
|
-
if (!racedAlreadyLinked) {
|
|
259
|
-
publishDocumentsChanged();
|
|
260
|
-
}
|
|
261
|
-
return loadDocumentPayload(raced.surfaceId);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
log.info(
|
|
265
|
-
{ surfaceId, workspacePath: relativePath, conversationId },
|
|
266
|
-
"Bound workspace file to a new document",
|
|
267
|
-
);
|
|
268
|
-
publishDocumentsChanged();
|
|
269
|
-
return loadDocumentPayload(surfaceId);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
83
|
// ---------------------------------------------------------------------------
|
|
273
84
|
// Route definitions
|
|
274
85
|
// ---------------------------------------------------------------------------
|
|
@@ -403,36 +214,6 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
403
214
|
},
|
|
404
215
|
},
|
|
405
216
|
|
|
406
|
-
{
|
|
407
|
-
operationId: "documentForWorkspaceFile",
|
|
408
|
-
endpoint: "documents/for-workspace-file",
|
|
409
|
-
method: "POST",
|
|
410
|
-
policy: {
|
|
411
|
-
requiredScopes: ["settings.write"],
|
|
412
|
-
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
413
|
-
},
|
|
414
|
-
summary: "Get or create the document for a workspace markdown file",
|
|
415
|
-
description:
|
|
416
|
-
"Return the document bound to a workspace markdown file, creating it on " +
|
|
417
|
-
"first open. The file is the source of truth at open time: stored " +
|
|
418
|
-
"content that has drifted from the file is refreshed from disk before " +
|
|
419
|
-
"the document is returned.",
|
|
420
|
-
tags: ["documents"],
|
|
421
|
-
requestBody: z.object({
|
|
422
|
-
path: z
|
|
423
|
-
.string()
|
|
424
|
-
.describe("Workspace-relative path to a .md/.markdown file"),
|
|
425
|
-
conversationId: z
|
|
426
|
-
.string()
|
|
427
|
-
.describe("Conversation the document is opened from"),
|
|
428
|
-
}),
|
|
429
|
-
responseBody: documentPayloadSchema,
|
|
430
|
-
additionalResponses: {
|
|
431
|
-
"422": { description: "Path is not a markdown file" },
|
|
432
|
-
},
|
|
433
|
-
handler: handleDocumentForWorkspaceFile,
|
|
434
|
-
},
|
|
435
|
-
|
|
436
217
|
{
|
|
437
218
|
operationId: "linkDocumentConversation",
|
|
438
219
|
endpoint: "documents/:id/conversations",
|
|
@@ -47,6 +47,7 @@ interface MockConversation {
|
|
|
47
47
|
contextCompactedAt: number | null;
|
|
48
48
|
sentMessages: AssistantEvent[];
|
|
49
49
|
sendToClient: (msg: AssistantEvent) => void;
|
|
50
|
+
emit: (msg: AssistantEvent) => void;
|
|
50
51
|
getMessages: () => unknown[];
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -64,6 +65,7 @@ function makeConversation(id = "conv-playground-test"): MockConversation {
|
|
|
64
65
|
contextCompactedAt: null,
|
|
65
66
|
sentMessages,
|
|
66
67
|
sendToClient: (msg) => sentMessages.push(msg),
|
|
68
|
+
emit: (msg) => sentMessages.push(msg),
|
|
67
69
|
getMessages: () => [],
|
|
68
70
|
};
|
|
69
71
|
}
|