@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
|
@@ -447,6 +447,13 @@ interface UtteranceCycle {
|
|
|
447
447
|
// to the silence-boundary path and stays there: a late event must not
|
|
448
448
|
// commit a boundary the fallback already owns.
|
|
449
449
|
providerTurnEndTimedOut: boolean;
|
|
450
|
+
// The provider's own end-of-turn released this cycle, so its transcript is
|
|
451
|
+
// already complete. A provider that decides turn ends commits the transcript
|
|
452
|
+
// in the same event, so such a release needs no caller-side flush and must
|
|
453
|
+
// not tear the stream down. False on every caller-side release (the fail-open
|
|
454
|
+
// deadline, a max-duration force-end, barge-in), where a turn may still be
|
|
455
|
+
// open provider-side and only a stream close can make it answer.
|
|
456
|
+
providerClosedTurn: boolean;
|
|
450
457
|
// `vadSpeechGeneration` as of the local silence boundary that handed this
|
|
451
458
|
// cycle to the provider, or null while no boundary has fired yet (a turn
|
|
452
459
|
// model routinely beats the trailing-silence countdown, and that fast commit
|
|
@@ -948,6 +955,7 @@ function createUtteranceCycle(): UtteranceCycle {
|
|
|
948
955
|
latestPartialText: null,
|
|
949
956
|
endpointExtensionCount: 0,
|
|
950
957
|
providerTurnEndTimedOut: false,
|
|
958
|
+
providerClosedTurn: false,
|
|
951
959
|
turnBoundaryGeneration: null,
|
|
952
960
|
openProviderTurnIndex: null,
|
|
953
961
|
heldSpeculativeContent: null,
|
|
@@ -1216,12 +1224,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1216
1224
|
// replayed once the parked speech flushes into the next armed utterance.
|
|
1217
1225
|
private vadPendingTurnEnd: "silence" | "max-duration" | null = null;
|
|
1218
1226
|
private readonly metricsClock: LiveVoiceMetricsClock;
|
|
1219
|
-
// Persistent mode: server-VAD
|
|
1220
|
-
//
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1224
|
-
//
|
|
1227
|
+
// Persistent mode: a server-VAD session keeps one streaming transcriber for
|
|
1228
|
+
// the whole session when the stream can be sealed per utterance without
|
|
1229
|
+
// closing it. Either the provider is finalize-capable (release flushes via
|
|
1230
|
+
// finalizeUtterance()) or the provider decides turn ends itself (release is
|
|
1231
|
+
// already sealed by the end-of-turn that triggered it). Re-arm reuses this
|
|
1232
|
+
// instance synchronously. Null in manual mode, for a stream that offers
|
|
1233
|
+
// neither seal, and after an unexpected stream close (the next arm resolves
|
|
1234
|
+
// a fresh transcriber).
|
|
1225
1235
|
private sharedTranscriber: StreamingTranscriber | null = null;
|
|
1226
1236
|
// The `services.stt.language` value the shared stream was dialed with.
|
|
1227
1237
|
// Providers pin the language per connection, so the persistent re-arm
|
|
@@ -1635,7 +1645,15 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1635
1645
|
);
|
|
1636
1646
|
if (
|
|
1637
1647
|
this.turnDetector &&
|
|
1638
|
-
typeof transcriber.finalizeUtterance === "function"
|
|
1648
|
+
(typeof transcriber.finalizeUtterance === "function" ||
|
|
1649
|
+
// A provider that owns the turn boundary needs no caller-side flush
|
|
1650
|
+
// on the normal path: its end-of-turn IS the flush, and it commits
|
|
1651
|
+
// the transcript in the same event. Gating persistence on
|
|
1652
|
+
// `finalizeUtterance` alone asks "can the caller flush this stream"
|
|
1653
|
+
// when the question is "will the caller ever have to", and answering
|
|
1654
|
+
// it wrong costs a socket per utterance, which drops the audio that
|
|
1655
|
+
// arrives while the next one dials.
|
|
1656
|
+
this.providerTurnEndActive)
|
|
1639
1657
|
) {
|
|
1640
1658
|
// Adopt persistent mode: this stream serves the whole session, so
|
|
1641
1659
|
// its events route by cycle ownership instead of binding to this
|
|
@@ -3455,6 +3473,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3455
3473
|
"provider",
|
|
3456
3474
|
);
|
|
3457
3475
|
await this.sendFrame({ type: "utterance_end", reason: "silence" });
|
|
3476
|
+
// The provider committed this cycle's transcript in the event being
|
|
3477
|
+
// handled, so the release below seals it in place instead of flushing or
|
|
3478
|
+
// closing the stream.
|
|
3479
|
+
utterance.providerClosedTurn = true;
|
|
3458
3480
|
await this.releaseUtterance();
|
|
3459
3481
|
// Leave the local detector idle, which is where every other commit path
|
|
3460
3482
|
// leaves it. A provider can close a turn while the trailing-silence
|
|
@@ -3723,8 +3745,24 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3723
3745
|
): Promise<void> {
|
|
3724
3746
|
const shared = this.sharedTranscriber;
|
|
3725
3747
|
if (shared && utterance.transcriber === shared) {
|
|
3726
|
-
|
|
3727
|
-
|
|
3748
|
+
if (typeof shared.finalizeUtterance === "function") {
|
|
3749
|
+
await this.finalizeUtteranceForRelease(utterance);
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
if (utterance.providerClosedTurn) {
|
|
3753
|
+
await this.sealProviderClosedUtterance(utterance);
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
// A caller-side boundary on a stream with no flush: only closing it can
|
|
3757
|
+
// make the provider answer for a turn still open, so this one release
|
|
3758
|
+
// costs the stream. The stream stays installed as the shared one across
|
|
3759
|
+
// the close: its flush `final` and its `closed` both arrive after
|
|
3760
|
+
// `stop()`, and the shared router drops events from a stream it no
|
|
3761
|
+
// longer recognizes. `closed` is what retires it, seals this cycle and
|
|
3762
|
+
// dispatches the turn, so clearing the reference here would strand the
|
|
3763
|
+
// cycle in `released` with the flushed transcript discarded. Nothing can
|
|
3764
|
+
// re-arm onto the closing stream in the meantime: the next arm waits on
|
|
3765
|
+
// the assistant turn, which waits on that same `closed`.
|
|
3728
3766
|
}
|
|
3729
3767
|
|
|
3730
3768
|
utterance.phase = "released";
|
|
@@ -3744,6 +3782,22 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3744
3782
|
await this.drainOutboundFrames();
|
|
3745
3783
|
}
|
|
3746
3784
|
|
|
3785
|
+
// Persistent-mode release for a provider-decided turn end: the event that
|
|
3786
|
+
// released this cycle already carried its final transcript, so there is
|
|
3787
|
+
// nothing to flush and no reason to close the stream. Sealing the phase is
|
|
3788
|
+
// the whole of the teardown: the same "transcript complete, stream open"
|
|
3789
|
+
// state the `finalized` path reaches, arrived at without a round trip.
|
|
3790
|
+
private async sealProviderClosedUtterance(
|
|
3791
|
+
utterance: UtteranceCycle,
|
|
3792
|
+
): Promise<void> {
|
|
3793
|
+
if (utterance.phase === "transcriber_closed") {
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
utterance.phase = "transcriber_closed";
|
|
3797
|
+
await this.startAssistantTurnIfReady();
|
|
3798
|
+
await this.drainOutboundFrames();
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3747
3801
|
// Persistent-mode release: flush the shared stream's buffered audio with
|
|
3748
3802
|
// finalizeUtterance() instead of tearing the stream down. The assistant
|
|
3749
3803
|
// turn starts on the `finalized` event; the grace timer bounds a flush
|
|
@@ -45,6 +45,7 @@ export async function spawnMonitoringWorkerProcess(
|
|
|
45
45
|
return await spawnWorkerProcess({
|
|
46
46
|
pidPath: getMonitoringPidPath(),
|
|
47
47
|
entry: new URL("./worker.ts", import.meta.url),
|
|
48
|
+
packagedEntry: "monitoring",
|
|
48
49
|
workerLabel: "Resource monitor",
|
|
49
50
|
options: opts,
|
|
50
51
|
});
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
20
|
|
|
22
21
|
import { getRawShareAnalytics } from "../platform/consent-cache.js";
|
|
23
22
|
import { recordWatchdogEvent } from "../telemetry/watchdog-events-store.js";
|
|
24
23
|
import { getLogger } from "../util/logger.js";
|
|
25
24
|
import { getDbPath, getMonitoringDataDir } from "../util/platform.js";
|
|
25
|
+
import { resolveWorkerCommand } from "../util/worker-process.js";
|
|
26
26
|
import type { IntegritySampleResult } from "./db-integrity-check.js";
|
|
27
27
|
|
|
28
28
|
const log = getLogger("db-integrity-sample");
|
|
@@ -60,13 +60,12 @@ let activeChild: ReturnType<typeof Bun.spawn> | null = null;
|
|
|
60
60
|
async function runCheckSubprocess(
|
|
61
61
|
dbPath: string,
|
|
62
62
|
): Promise<IntegritySampleResult | null> {
|
|
63
|
-
|
|
64
|
-
// install path with a space in it would not resolve. See worker-process.ts.
|
|
65
|
-
const entry = fileURLToPath(
|
|
63
|
+
const command = resolveWorkerCommand(
|
|
66
64
|
new URL("./db-integrity-check.ts", import.meta.url),
|
|
65
|
+
"db-integrity",
|
|
67
66
|
);
|
|
68
67
|
const child = Bun.spawn({
|
|
69
|
-
cmd: [
|
|
68
|
+
cmd: [...command, dbPath],
|
|
70
69
|
stdio: ["ignore", "pipe", "ignore"],
|
|
71
70
|
});
|
|
72
71
|
activeChild = child;
|
|
@@ -9,3 +9,5 @@ When a notification flow creates a server-side conversation (e.g. guardian quest
|
|
|
9
9
|
Guardian-request producers (access requests, tool approvals, tool-grant escalations, voice questions, trusted-contact confirmations) **MUST** record approval-card deliveries through `recordApprovalCardDelivery` / `recordGuardianRequestDeliveries` in `guardian-delivery-recorder.ts` -- never by calling the gateway client's `createGuardianRequestDelivery` directly. That sink is the single place the card-to-request addressing convention lives (conversation id for the in-app card; chat + channel-native `ts` for channel cards), so the path that writes a delivery row cannot drift from the paths that read it back to withdraw a card or resolve an emoji reaction -- the drift that previously fanned this logic into four divergent copies. (Tests may seed delivery rows directly for fixtures.)
|
|
10
10
|
|
|
11
11
|
Approval-card **source references** (the link back to the channel message that triggered a request) resolve only through `resolveApprovalSourceReference()` in `runtime/approval-source-link.ts` -- producers spread the result into the `guardian.question` context payload and never hand-build links. Channel-format knowledge (id shapes, permalinks, mrkdwn) lives only in `messaging/providers/<channel>/` and `notifications/adapters/<channel>`; the four-layer ownership map is documented at the top of `approval-source-link.ts`. Exception: access-request cards predate the registry and still derive their Slack permalink from payload `messageTs` in `access-request-copy.ts` -- converge them onto the registry rather than adding a third resolution path.
|
|
12
|
+
|
|
13
|
+
Guardian-request card rows are **not conversation history**. `pairDeliveryWithConversation` persists a message row per delivery, and for a guardian card that row is addressed to a conversation the request is _about_ (the vellum card is pinned to the originating conversation; a channel card lands in whatever conversation the guardian's chat binds to). `isGuardianCardRow` in `approval-card-data.ts` is the single definition of which rows those are, derived from the card's own `ui_surface` id rather than a stored marker so old rows need no backfill. **Both** history assemblers must consult it -- `Conversation.loadFromDb` and `loadSlackChronologicalContext`, which re-reads rows rather than using `this.messages` -- or the unfiltered one replays the card between a parked turn's `tool_use` and its `tool_result` and history repair destroys the real result. Surface state is exempt on purpose: the card's buttons must still route after a restart. Full rationale in [docs/guardian-request-flow.md](../../docs/guardian-request-flow.md).
|
|
@@ -75,6 +75,39 @@ export function approvalCardSurfaceId(
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Whether a persisted message row is a guardian card rather than the
|
|
80
|
+
* conversation's own speech.
|
|
81
|
+
*
|
|
82
|
+
* Read off the card's own `ui_surface` id, using the prefixes above. The card
|
|
83
|
+
* builder writes that block on every path (`ensureSeedContentBlocks`), and the
|
|
84
|
+
* withdrawal path already recomputes the same id from the same constants, so
|
|
85
|
+
* the identity is structural rather than sniffed. Deriving it means rows
|
|
86
|
+
* written before this rule existed are recognized on the same terms as new
|
|
87
|
+
* ones, with no marker to backfill.
|
|
88
|
+
*
|
|
89
|
+
* Only a row that LEADS with an approval-card surface counts. A real assistant
|
|
90
|
+
* turn can carry a `ui_surface` block too (a wake card is unshifted onto the
|
|
91
|
+
* turn's own content), and those surfaces do not use these prefixes; requiring
|
|
92
|
+
* the lead position keeps the rule from ever swallowing a turn that has speech
|
|
93
|
+
* in it.
|
|
94
|
+
*/
|
|
95
|
+
export function isGuardianCardRow(
|
|
96
|
+
content: readonly unknown[] | string | null | undefined,
|
|
97
|
+
): boolean {
|
|
98
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const lead = content[0] as { type?: unknown; surfaceId?: unknown };
|
|
102
|
+
if (lead?.type !== "ui_surface" || typeof lead.surfaceId !== "string") {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return (
|
|
106
|
+
lead.surfaceId.startsWith(`${ACCESS_REQUEST_SURFACE_PREFIX}-`) ||
|
|
107
|
+
lead.surfaceId.startsWith(`${TOOL_APPROVAL_SURFACE_PREFIX}-`)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
78
111
|
// ── Typed card data ─────────────────────────────────────────────────────────
|
|
79
112
|
|
|
80
113
|
/** Resolved card data for an access-request notification. */
|
|
@@ -36,7 +36,7 @@ export class PermissionPrompter {
|
|
|
36
36
|
* pendingInteractions, matching the host proxy pattern.
|
|
37
37
|
*/
|
|
38
38
|
private ownedIds = new Set<string>();
|
|
39
|
-
private sendToClient: (msg: AssistantEvent) => void;
|
|
39
|
+
private readonly sendToClient: (msg: AssistantEvent) => void;
|
|
40
40
|
private onStateChanged?: ConfirmationStateCallback;
|
|
41
41
|
|
|
42
42
|
constructor(sendToClient: (msg: AssistantEvent) => void) {
|
|
@@ -47,10 +47,6 @@ export class PermissionPrompter {
|
|
|
47
47
|
this.onStateChanged = cb;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
updateSender(sendToClient: (msg: AssistantEvent) => void): void {
|
|
51
|
-
this.sendToClient = sendToClient;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
50
|
async prompt(
|
|
55
51
|
toolName: string,
|
|
56
52
|
input: Record<string, unknown>,
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
and,
|
|
3
|
+
count,
|
|
4
|
+
desc,
|
|
5
|
+
eq,
|
|
6
|
+
getTableColumns,
|
|
7
|
+
isNull,
|
|
8
|
+
lt,
|
|
9
|
+
sql,
|
|
10
|
+
} from "drizzle-orm";
|
|
11
|
+
import type { SQLiteSelect } from "drizzle-orm/sqlite-core";
|
|
2
12
|
|
|
3
13
|
import {
|
|
4
14
|
parseExternalContentEnvelope,
|
|
@@ -263,6 +273,15 @@ export interface ConversationListFilter {
|
|
|
263
273
|
* section, and a UUID one custom group. Omit to span every group.
|
|
264
274
|
*/
|
|
265
275
|
groupId?: string;
|
|
276
|
+
/**
|
|
277
|
+
* Restrict to conversations whose latest assistant message the user has
|
|
278
|
+
* not seen: the same "unseen" predicate the unread count and the section
|
|
279
|
+
* index apply ({@link unseenAttentionStateConditions}), so every reader
|
|
280
|
+
* of "needs attention" agrees. Only `true` narrows; omit to span every
|
|
281
|
+
* conversation. A client that keeps no complete list can ask for exactly
|
|
282
|
+
* the rows its attention surfaces need instead of scanning for them.
|
|
283
|
+
*/
|
|
284
|
+
needsAttention?: true;
|
|
266
285
|
}
|
|
267
286
|
|
|
268
287
|
export interface ConversationListQuery extends ConversationListFilter {
|
|
@@ -327,21 +346,55 @@ function originChannelClause(originChannel: string) {
|
|
|
327
346
|
return eq(conversations.originChannel, originChannel);
|
|
328
347
|
}
|
|
329
348
|
|
|
349
|
+
/**
|
|
350
|
+
* The join between a conversation and its attention projection, one row per
|
|
351
|
+
* conversation in `conversation_assistant_attention_state`. Every reader of
|
|
352
|
+
* "unseen" joins on this: the list and count filters, the unread count, and
|
|
353
|
+
* the section index.
|
|
354
|
+
*/
|
|
355
|
+
const attentionJoin = eq(
|
|
356
|
+
conversationAssistantAttentionState.conversationId,
|
|
357
|
+
conversations.id,
|
|
358
|
+
);
|
|
359
|
+
|
|
330
360
|
function conversationListWhere(filter: ConversationListFilter) {
|
|
331
361
|
const {
|
|
332
362
|
conversationType = "standard",
|
|
333
363
|
archiveStatus = "active",
|
|
334
364
|
originChannel,
|
|
335
365
|
groupId,
|
|
366
|
+
needsAttention,
|
|
336
367
|
} = filter;
|
|
337
368
|
return and(
|
|
338
369
|
conversationTypeClause(conversationType),
|
|
339
370
|
archiveStatusClause(archiveStatus) ?? undefined,
|
|
340
371
|
originChannel ? originChannelClause(originChannel) : undefined,
|
|
341
372
|
groupId ? groupIdClause(groupId) : undefined,
|
|
373
|
+
...(needsAttention ? unseenAttentionStateConditions() : []),
|
|
342
374
|
);
|
|
343
375
|
}
|
|
344
376
|
|
|
377
|
+
/**
|
|
378
|
+
* The joins {@link conversationListWhere}'s predicates need, applied to a
|
|
379
|
+
* list or count query over `conversations`. The attention predicate lives on
|
|
380
|
+
* `conversation_assistant_attention_state`, so `needsAttention` joins it.
|
|
381
|
+
* Inner, not left: a conversation with no
|
|
382
|
+
* attention row has no unseen message by definition. Joined only when the
|
|
383
|
+
* filter asks, so a query without the filter runs without the join and
|
|
384
|
+
* lists every conversation the predicates admit. One place for both
|
|
385
|
+
* {@link listConversations} and {@link countConversations}, so a page and
|
|
386
|
+
* its total always describe the same set.
|
|
387
|
+
*/
|
|
388
|
+
function withListFilterJoins<T extends SQLiteSelect>(
|
|
389
|
+
query: T,
|
|
390
|
+
filter: ConversationListFilter,
|
|
391
|
+
) {
|
|
392
|
+
if (!filter.needsAttention) {
|
|
393
|
+
return query;
|
|
394
|
+
}
|
|
395
|
+
return query.innerJoin(conversationAssistantAttentionState, attentionJoin);
|
|
396
|
+
}
|
|
397
|
+
|
|
345
398
|
export function listConversations(
|
|
346
399
|
query: ConversationListQuery = {},
|
|
347
400
|
): ConversationRow[] {
|
|
@@ -362,9 +415,13 @@ export function listConversations(
|
|
|
362
415
|
// distinguish a total order from a lucky one. The guarantee becomes
|
|
363
416
|
// observable, and gets its test, with the keyset pagination work.
|
|
364
417
|
const tiebreak = desc(conversations.id);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
418
|
+
// Selecting the conversation columns explicitly keeps the row shape
|
|
419
|
+
// `parseConversation` maps identical whether or not the attention join is
|
|
420
|
+
// present.
|
|
421
|
+
return withListFilterJoins(
|
|
422
|
+
db.select(getTableColumns(conversations)).from(conversations).$dynamic(),
|
|
423
|
+
filter,
|
|
424
|
+
)
|
|
368
425
|
.where(conversationListWhere(filter))
|
|
369
426
|
.orderBy(recency, tiebreak)
|
|
370
427
|
.limit(limit ?? 100)
|
|
@@ -542,9 +599,10 @@ export function countConversations(
|
|
|
542
599
|
): number {
|
|
543
600
|
ensureGroupMigration();
|
|
544
601
|
const db = getDb();
|
|
545
|
-
const [{ total }] =
|
|
546
|
-
.select({ total: count() })
|
|
547
|
-
|
|
602
|
+
const [{ total }] = withListFilterJoins(
|
|
603
|
+
db.select({ total: count() }).from(conversations).$dynamic(),
|
|
604
|
+
filter,
|
|
605
|
+
)
|
|
548
606
|
.where(conversationListWhere(filter))
|
|
549
607
|
.all();
|
|
550
608
|
return total;
|
|
@@ -582,10 +640,7 @@ export function countUnreadConversations(): number {
|
|
|
582
640
|
const [{ total }] = db
|
|
583
641
|
.select({ total: count() })
|
|
584
642
|
.from(conversations)
|
|
585
|
-
.innerJoin(
|
|
586
|
-
conversationAssistantAttentionState,
|
|
587
|
-
eq(conversationAssistantAttentionState.conversationId, conversations.id),
|
|
588
|
-
)
|
|
643
|
+
.innerJoin(conversationAssistantAttentionState, attentionJoin)
|
|
589
644
|
.where(
|
|
590
645
|
and(
|
|
591
646
|
conversationTypeClause("standard"),
|
|
@@ -647,11 +702,6 @@ export function countConversationSections(): ConversationSectionCounts {
|
|
|
647
702
|
...unseenAttentionStateConditions(),
|
|
648
703
|
)} THEN 1 ELSE 0 END)`;
|
|
649
704
|
|
|
650
|
-
const attentionJoin = eq(
|
|
651
|
-
conversationAssistantAttentionState.conversationId,
|
|
652
|
-
conversations.id,
|
|
653
|
-
);
|
|
654
|
-
|
|
655
705
|
const groups = db
|
|
656
706
|
.select({
|
|
657
707
|
groupId: sql<string>`group_id`,
|
|
@@ -33,6 +33,45 @@ const SHUTDOWN_GRACE_MS = 5_000;
|
|
|
33
33
|
const STDOUT_CAPTURE_LIMIT = 16_384;
|
|
34
34
|
const STDERR_CAPTURE_LIMIT = 4_096;
|
|
35
35
|
|
|
36
|
+
export function resolveQdrantReleaseAsset(
|
|
37
|
+
os: NodeJS.Platform,
|
|
38
|
+
cpu: string,
|
|
39
|
+
): {
|
|
40
|
+
binaryName: string;
|
|
41
|
+
filename: string;
|
|
42
|
+
format: "tar.gz" | "zip";
|
|
43
|
+
sha256?: string;
|
|
44
|
+
} {
|
|
45
|
+
let target: string;
|
|
46
|
+
if (os === "darwin" && cpu === "arm64") {
|
|
47
|
+
target = "aarch64-apple-darwin";
|
|
48
|
+
} else if (os === "darwin" && cpu === "x64") {
|
|
49
|
+
target = "x86_64-apple-darwin";
|
|
50
|
+
} else if (os === "linux" && cpu === "x64") {
|
|
51
|
+
target = "x86_64-unknown-linux-musl";
|
|
52
|
+
} else if (os === "linux" && cpu === "arm64") {
|
|
53
|
+
target = "aarch64-unknown-linux-musl";
|
|
54
|
+
} else if (os === "win32" && cpu === "x64") {
|
|
55
|
+
return {
|
|
56
|
+
binaryName: "qdrant.exe",
|
|
57
|
+
filename: "qdrant-x86_64-pc-windows-msvc.zip",
|
|
58
|
+
format: "zip",
|
|
59
|
+
sha256:
|
|
60
|
+
"9d3b1d1fa58566bc71709347c4c9b83a0111d23a550daa93c3ee48e3150c4470",
|
|
61
|
+
};
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Unsupported platform: ${os}/${cpu}. ` +
|
|
65
|
+
"Set QDRANT_URL to use an external Qdrant instance.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
binaryName: "qdrant",
|
|
70
|
+
filename: `qdrant-${target}.tar.gz`,
|
|
71
|
+
format: "tar.gz",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
36
75
|
export interface QdrantManagerConfig {
|
|
37
76
|
url: string;
|
|
38
77
|
storagePath?: string;
|
|
@@ -181,34 +220,18 @@ export class QdrantManager {
|
|
|
181
220
|
private async installBinary(binaryPath: string): Promise<void> {
|
|
182
221
|
const os = platform();
|
|
183
222
|
const cpu = arch();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (os === "darwin" && cpu === "arm64") {
|
|
187
|
-
target = "aarch64-apple-darwin";
|
|
188
|
-
} else if (os === "darwin" && cpu === "x64") {
|
|
189
|
-
target = "x86_64-apple-darwin";
|
|
190
|
-
} else if (os === "linux" && cpu === "x64") {
|
|
191
|
-
target = "x86_64-unknown-linux-musl";
|
|
192
|
-
} else if (os === "linux" && cpu === "arm64") {
|
|
193
|
-
target = "aarch64-unknown-linux-musl";
|
|
194
|
-
} else {
|
|
195
|
-
throw new Error(
|
|
196
|
-
`Unsupported platform: ${os}/${cpu}. ` +
|
|
197
|
-
"Set QDRANT_URL to use an external Qdrant instance.",
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const filename = `qdrant-${target}.tar.gz`;
|
|
223
|
+
const release = resolveQdrantReleaseAsset(os, cpu);
|
|
224
|
+
const { filename } = release;
|
|
202
225
|
const baseUrl = `https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}`;
|
|
203
226
|
const url = `${baseUrl}/${filename}`;
|
|
204
227
|
const checksumUrl = `${baseUrl}/${filename}.sha256`;
|
|
205
228
|
|
|
206
229
|
log.info({ url, binaryPath }, "Downloading Qdrant binary");
|
|
207
230
|
|
|
208
|
-
// Fetch the
|
|
231
|
+
// Fetch the archive and its SHA-256 checksum in parallel
|
|
209
232
|
const [response, checksumResponse] = await Promise.all([
|
|
210
233
|
fetch(url),
|
|
211
|
-
fetch(checksumUrl),
|
|
234
|
+
release.sha256 ? Promise.resolve(null) : fetch(checksumUrl),
|
|
212
235
|
]);
|
|
213
236
|
|
|
214
237
|
if (!response.ok) {
|
|
@@ -217,15 +240,17 @@ export class QdrantManager {
|
|
|
217
240
|
);
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
const
|
|
243
|
+
const archive = await response.arrayBuffer();
|
|
221
244
|
|
|
222
|
-
// Verify SHA-256 integrity if
|
|
223
|
-
if (checksumResponse
|
|
224
|
-
const checksumText =
|
|
245
|
+
// Verify SHA-256 integrity if a pinned or published checksum is available
|
|
246
|
+
if (release.sha256 || checksumResponse?.ok) {
|
|
247
|
+
const checksumText = release.sha256
|
|
248
|
+
? release.sha256
|
|
249
|
+
: (await checksumResponse!.text()).trim();
|
|
225
250
|
// Checksum files contain "<hex> <filename>" or just "<hex>"
|
|
226
251
|
const expectedHash = checksumText.split(/\s+/)[0].toLowerCase();
|
|
227
252
|
const actualHash = createHash("sha256")
|
|
228
|
-
.update(Buffer.from(
|
|
253
|
+
.update(Buffer.from(archive))
|
|
229
254
|
.digest("hex");
|
|
230
255
|
|
|
231
256
|
if (actualHash !== expectedHash) {
|
|
@@ -237,39 +262,45 @@ export class QdrantManager {
|
|
|
237
262
|
log.info({ hash: actualHash }, "Qdrant binary checksum verified");
|
|
238
263
|
} else {
|
|
239
264
|
log.warn(
|
|
240
|
-
{ checksumUrl, status: checksumResponse
|
|
241
|
-
"Could not fetch Qdrant checksum
|
|
265
|
+
{ checksumUrl, status: checksumResponse?.status },
|
|
266
|
+
"Could not fetch Qdrant checksum; skipping integrity check",
|
|
242
267
|
);
|
|
243
268
|
}
|
|
244
269
|
|
|
245
|
-
// Extract the qdrant binary from the tarball
|
|
246
270
|
const binDir = dirname(binaryPath);
|
|
247
271
|
mkdirSync(binDir, { recursive: true });
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const proc = Bun.spawn({
|
|
255
|
-
cmd: ["tar", "xzf", tmpTar, "-C", binDir, "qdrant"],
|
|
256
|
-
stdout: "ignore",
|
|
257
|
-
stderr: "pipe",
|
|
258
|
-
});
|
|
259
|
-
await proc.exited;
|
|
260
|
-
if (proc.exitCode !== 0) {
|
|
261
|
-
const stderr = await new Response(proc.stderr).text();
|
|
262
|
-
throw new Error(`Failed to extract Qdrant binary: ${stderr}`);
|
|
272
|
+
if (release.format === "zip") {
|
|
273
|
+
const { default: JSZip } = await import("jszip");
|
|
274
|
+
const zip = await JSZip.loadAsync(archive);
|
|
275
|
+
const entry = zip.file(release.binaryName);
|
|
276
|
+
if (!entry) {
|
|
277
|
+
throw new Error(`${filename} does not contain ${release.binaryName}.`);
|
|
263
278
|
}
|
|
264
|
-
|
|
279
|
+
writeFileSync(binaryPath, await entry.async("nodebuffer"));
|
|
280
|
+
} else {
|
|
281
|
+
const tmpTar = join(binDir, `qdrant-download-${Date.now()}.tar.gz`);
|
|
282
|
+
writeFileSync(tmpTar, Buffer.from(archive));
|
|
265
283
|
try {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
284
|
+
const proc = Bun.spawn({
|
|
285
|
+
cmd: ["tar", "xzf", tmpTar, "-C", binDir, release.binaryName],
|
|
286
|
+
stdout: "ignore",
|
|
287
|
+
stderr: "pipe",
|
|
288
|
+
});
|
|
289
|
+
await proc.exited;
|
|
290
|
+
if (proc.exitCode !== 0) {
|
|
291
|
+
const stderr = await new Response(proc.stderr).text();
|
|
292
|
+
throw new Error(`Failed to extract Qdrant binary: ${stderr}`);
|
|
293
|
+
}
|
|
294
|
+
} finally {
|
|
295
|
+
try {
|
|
296
|
+
unlinkSync(tmpTar);
|
|
297
|
+
} catch {}
|
|
269
298
|
}
|
|
270
299
|
}
|
|
271
300
|
|
|
272
|
-
|
|
301
|
+
if (os !== "win32") {
|
|
302
|
+
chmodSync(binaryPath, 0o755);
|
|
303
|
+
}
|
|
273
304
|
log.info(
|
|
274
305
|
{ binaryPath, version: QDRANT_VERSION },
|
|
275
306
|
"Qdrant binary installed",
|
|
@@ -395,7 +426,8 @@ export class QdrantManager {
|
|
|
395
426
|
}
|
|
396
427
|
|
|
397
428
|
private getBinaryPath(): string {
|
|
398
|
-
|
|
429
|
+
const binaryName = platform() === "win32" ? "qdrant.exe" : "qdrant";
|
|
430
|
+
return join(getDataDir(), "qdrant", "bin", binaryName);
|
|
399
431
|
}
|
|
400
432
|
|
|
401
433
|
private cleanupStaleProcess(): void {
|
|
@@ -447,6 +479,9 @@ export class QdrantManager {
|
|
|
447
479
|
* discoverable by tools that scan for "vellum" in process names.
|
|
448
480
|
*/
|
|
449
481
|
private ensureVellumSymlink(binaryPath: string): string {
|
|
482
|
+
if (platform() === "win32") {
|
|
483
|
+
return binaryPath;
|
|
484
|
+
}
|
|
450
485
|
const symlinkPath = join(dirname(binaryPath), "vellum-qdrant");
|
|
451
486
|
const expectedTarget = realpathSync(binaryPath);
|
|
452
487
|
|
|
@@ -7,20 +7,11 @@ const COLUMN_DEFINITION = "workspace_path TEXT";
|
|
|
7
7
|
/**
|
|
8
8
|
* Add `workspace_path` to `documents` and enforce one document per file.
|
|
9
9
|
*
|
|
10
|
-
* The column
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* drafts, client-created surfaces) stay NULL.
|
|
16
|
-
*
|
|
17
|
-
* The unique index is partial (`WHERE workspace_path IS NOT NULL`) so the many
|
|
18
|
-
* NULL rows stay unconstrained while a given file resolves to exactly one
|
|
19
|
-
* document. The find-or-create route depends on that uniqueness for its
|
|
20
|
-
* idempotency.
|
|
21
|
-
*
|
|
22
|
-
* Nullable with no backfill: nothing recorded a file binding before this
|
|
23
|
-
* column existed, so pre-existing rows correctly stay NULL.
|
|
10
|
+
* The column and its partial unique index are unused by the daemon: no code
|
|
11
|
+
* reads or writes them. Some rows carry a non-NULL path; new rows stay NULL.
|
|
12
|
+
* The migration stays in the chain because migrations are append-only, and the
|
|
13
|
+
* drizzle model in `schema/documents.ts` carries the column so it keeps
|
|
14
|
+
* describing the physical table.
|
|
24
15
|
*
|
|
25
16
|
* Idempotent: the column add is guarded with `tableHasColumn` and the index
|
|
26
17
|
* with `IF NOT EXISTS`, so a crash between the two does not break the next
|
|
@@ -22,13 +22,13 @@ export const documents = sqliteTable(
|
|
|
22
22
|
wordCount: integer("word_count").notNull().default(0),
|
|
23
23
|
createdAt: integer("created_at").notNull(),
|
|
24
24
|
updatedAt: integer("updated_at").notNull(),
|
|
25
|
-
//
|
|
26
|
-
// NULL
|
|
25
|
+
// Unused by the daemon: no code reads or writes the column, and some rows
|
|
26
|
+
// carry a non-NULL path. It stays in the model because migration 360 puts
|
|
27
|
+
// it in the physical table and the model must keep describing that table.
|
|
27
28
|
workspacePath: text("workspace_path"),
|
|
28
29
|
},
|
|
29
30
|
(table) => [
|
|
30
|
-
// Partial so the
|
|
31
|
-
// file resolves to exactly one document.
|
|
31
|
+
// Partial, so the NULL rows stay unconstrained.
|
|
32
32
|
uniqueIndex("idx_documents_workspace_path")
|
|
33
33
|
.on(table.workspacePath)
|
|
34
34
|
.where(sql`workspace_path IS NOT NULL`),
|
|
@@ -39,6 +39,14 @@ export const HOOKS = {
|
|
|
39
39
|
/** Union of every hook name declared in {@link HOOKS}. */
|
|
40
40
|
export type HookName = (typeof HOOKS)[keyof typeof HOOKS];
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Message kind for the hidden user row that starts a voice escalation leg.
|
|
44
|
+
* Hooks use this marker to distinguish the continuation instruction from
|
|
45
|
+
* other transcript-suppressed prompts without depending on host voice code.
|
|
46
|
+
*/
|
|
47
|
+
export const VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND =
|
|
48
|
+
"voice_escalation_continuation";
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
51
|
* Appended (inside the `<system_notice>` wrapper) to the internal continuation /
|
|
44
52
|
* completion nudge strings that re-query the model. Those notices are injected
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -67,7 +67,11 @@
|
|
|
67
67
|
*/
|
|
68
68
|
|
|
69
69
|
export type { HookName } from "./constants.js";
|
|
70
|
-
export {
|
|
70
|
+
export {
|
|
71
|
+
HOOKS,
|
|
72
|
+
INTERNAL_NUDGE_OUTPUT_SUPPRESSION,
|
|
73
|
+
VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
|
|
74
|
+
} from "./constants.js";
|
|
71
75
|
// Conversation message/content shapes. A hook receives the live message
|
|
72
76
|
// history (e.g. `PostToolUseContext.latestMessages: Message[]`), so plugins
|
|
73
77
|
// that inspect or narrow content blocks — reading a `tool_use` block's input,
|