@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.3
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/knip.json +3 -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-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-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__/gateway-only-guard.test.ts +2 -5
- 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__/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/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-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 +81 -44
- 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/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 +8 -6
- package/src/tools/acp/spawn.ts +6 -4
- 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
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* without going through the HTTP layer.
|
|
7
7
|
*/
|
|
8
8
|
import { rawAll, rawGet, rawRun } from "../persistence/raw-query.js";
|
|
9
|
-
import { writeWorkspaceFile } from "../runtime/routes/workspace-utils.js";
|
|
10
9
|
import { getLogger } from "../util/logger.js";
|
|
11
10
|
|
|
12
11
|
const log = getLogger("document-store");
|
|
@@ -24,18 +23,13 @@ export interface DocumentRecord {
|
|
|
24
23
|
wordCount: number;
|
|
25
24
|
createdAt: number;
|
|
26
25
|
updatedAt: number;
|
|
27
|
-
/**
|
|
28
|
-
* Workspace-relative path of the markdown file this document is bound to,
|
|
29
|
-
* or `null` when the document has no file behind it.
|
|
30
|
-
*/
|
|
31
|
-
workspacePath: string | null;
|
|
32
26
|
}
|
|
33
27
|
|
|
34
28
|
/**
|
|
35
|
-
* The listing projection:
|
|
36
|
-
*
|
|
29
|
+
* The listing projection: everything but the body, which the list and search
|
|
30
|
+
* queries deliberately leave out of their SELECTs.
|
|
37
31
|
*/
|
|
38
|
-
export type DocumentSummary = Omit<DocumentRecord, "content"
|
|
32
|
+
export type DocumentSummary = Omit<DocumentRecord, "content">;
|
|
39
33
|
|
|
40
34
|
/** Words in a markdown body, the count the `word_count` column stores. */
|
|
41
35
|
function countWords(content: string): number {
|
|
@@ -72,10 +66,9 @@ interface DocumentRow {
|
|
|
72
66
|
word_count: number;
|
|
73
67
|
created_at: number;
|
|
74
68
|
updated_at: number;
|
|
75
|
-
workspace_path: string | null;
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
type DocumentListRow = Omit<DocumentRow, "content"
|
|
71
|
+
type DocumentListRow = Omit<DocumentRow, "content">;
|
|
79
72
|
|
|
80
73
|
function escapeSqlLikePattern(value: string): string {
|
|
81
74
|
return value.replace(/[\\%_]/g, "\\$&");
|
|
@@ -90,7 +83,6 @@ function mapRowToRecord(row: DocumentRow): DocumentRecord {
|
|
|
90
83
|
wordCount: row.word_count,
|
|
91
84
|
createdAt: row.created_at,
|
|
92
85
|
updatedAt: row.updated_at,
|
|
93
|
-
workspacePath: row.workspace_path,
|
|
94
86
|
};
|
|
95
87
|
}
|
|
96
88
|
|
|
@@ -99,7 +91,7 @@ export function getDocumentById(surfaceId: string): DocumentRecord | null {
|
|
|
99
91
|
try {
|
|
100
92
|
const row = rawGet<DocumentRow>(
|
|
101
93
|
"documents:getDocumentById",
|
|
102
|
-
/*sql*/ `SELECT surface_id, conversation_id, title, content, word_count, created_at, updated_at
|
|
94
|
+
/*sql*/ `SELECT surface_id, conversation_id, title, content, word_count, created_at, updated_at
|
|
103
95
|
FROM documents
|
|
104
96
|
WHERE surface_id = ?`,
|
|
105
97
|
surfaceId,
|
|
@@ -118,29 +110,6 @@ export function getDocumentById(surfaceId: string): DocumentRecord | null {
|
|
|
118
110
|
}
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
/**
|
|
122
|
-
* Look up the document bound to a workspace file. `workspacePath` must be the
|
|
123
|
-
* normalized workspace-relative path stored on the row. Returns `null` when no
|
|
124
|
-
* document is bound to that file.
|
|
125
|
-
*/
|
|
126
|
-
export function getDocumentByWorkspacePath(
|
|
127
|
-
workspacePath: string,
|
|
128
|
-
): DocumentRecord | null {
|
|
129
|
-
try {
|
|
130
|
-
const row = rawGet<DocumentRow>(
|
|
131
|
-
"documents:getDocumentByWorkspacePath",
|
|
132
|
-
/*sql*/ `SELECT surface_id, conversation_id, title, content, word_count, created_at, updated_at, workspace_path
|
|
133
|
-
FROM documents
|
|
134
|
-
WHERE workspace_path = ?`,
|
|
135
|
-
workspacePath,
|
|
136
|
-
);
|
|
137
|
-
return row ? mapRowToRecord(row) : null;
|
|
138
|
-
} catch (error) {
|
|
139
|
-
log.error({ err: error, workspacePath }, "Load-by-workspace-path error");
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
113
|
/** Return true when a document is associated with a conversation. */
|
|
145
114
|
export function isDocumentAssociatedWithConversation(
|
|
146
115
|
surfaceId: string,
|
|
@@ -429,202 +398,6 @@ export function findInDocument(
|
|
|
429
398
|
}
|
|
430
399
|
}
|
|
431
400
|
|
|
432
|
-
// ---------------------------------------------------------------------------
|
|
433
|
-
// Workspace file binding
|
|
434
|
-
// ---------------------------------------------------------------------------
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Mirror `content` onto the workspace file a document is bound to.
|
|
438
|
-
*
|
|
439
|
-
* This is the single write-through choke point: every path that persists
|
|
440
|
-
* document content — {@link saveDocument} (client save and `document_create`),
|
|
441
|
-
* {@link updateDocumentContent} (`document_update`), and
|
|
442
|
-
* {@link replaceInDocument} (`document_replace_text`) — calls it immediately
|
|
443
|
-
* before its own row write, so a file-backed document's markdown on disk always
|
|
444
|
-
* matches the stored row.
|
|
445
|
-
*
|
|
446
|
-
* The file is written first and a failure throws: a rejected write aborts the
|
|
447
|
-
* update before the row changes rather than leaving the two silently diverged.
|
|
448
|
-
* Documents with no file behind them are a no-op.
|
|
449
|
-
*/
|
|
450
|
-
function writeThroughToWorkspaceFile(surfaceId: string, content: string): void {
|
|
451
|
-
const row = rawGet<{ workspace_path: string | null }>(
|
|
452
|
-
"documents:writeThroughToWorkspaceFile:getPath",
|
|
453
|
-
/*sql*/ `SELECT workspace_path FROM documents WHERE surface_id = ?`,
|
|
454
|
-
surfaceId,
|
|
455
|
-
);
|
|
456
|
-
const workspacePath = row?.workspace_path;
|
|
457
|
-
if (!workspacePath) {
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
writeWorkspaceFile(workspacePath, Buffer.from(content, "utf-8"));
|
|
463
|
-
log.info({ surfaceId, workspacePath }, "Wrote document through to file");
|
|
464
|
-
} catch (error) {
|
|
465
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
466
|
-
throw new Error(
|
|
467
|
-
`Failed to write file-backed document to ${workspacePath}: ${message}`,
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Create a document seeded from a workspace markdown file and bind it to that
|
|
474
|
-
* file. The returned surface ID is the file's document identity — comments,
|
|
475
|
-
* assistant iteration, and PDF export all key off it.
|
|
476
|
-
*
|
|
477
|
-
* The partial unique index on `workspace_path` makes a concurrent second
|
|
478
|
-
* create fail rather than produce a duplicate binding; callers re-read by path
|
|
479
|
-
* on failure.
|
|
480
|
-
*/
|
|
481
|
-
export function createFileBackedDocument(params: {
|
|
482
|
-
surfaceId: string;
|
|
483
|
-
conversationId: string;
|
|
484
|
-
title: string;
|
|
485
|
-
content: string;
|
|
486
|
-
workspacePath: string;
|
|
487
|
-
}): { success: true } | { success: false; error: string } {
|
|
488
|
-
try {
|
|
489
|
-
const now = Date.now();
|
|
490
|
-
rawRun(
|
|
491
|
-
"documents:createFileBackedDocument",
|
|
492
|
-
/*sql*/ `INSERT INTO documents (surface_id, conversation_id, title, content, word_count, created_at, updated_at, workspace_path)
|
|
493
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
494
|
-
params.surfaceId,
|
|
495
|
-
params.conversationId,
|
|
496
|
-
params.title,
|
|
497
|
-
params.content,
|
|
498
|
-
countWords(params.content),
|
|
499
|
-
now,
|
|
500
|
-
now,
|
|
501
|
-
params.workspacePath,
|
|
502
|
-
);
|
|
503
|
-
addDocumentConversation(params.surfaceId, params.conversationId);
|
|
504
|
-
log.info(
|
|
505
|
-
{ surfaceId: params.surfaceId, workspacePath: params.workspacePath },
|
|
506
|
-
"Created file-backed document",
|
|
507
|
-
);
|
|
508
|
-
return { success: true };
|
|
509
|
-
} catch (error) {
|
|
510
|
-
log.error(
|
|
511
|
-
{ err: error, workspacePath: params.workspacePath },
|
|
512
|
-
"File-backed document create error",
|
|
513
|
-
);
|
|
514
|
-
return {
|
|
515
|
-
success: false,
|
|
516
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Move file-backed documents onto the path a workspace rename moved their file
|
|
523
|
-
* to. `oldPath` and `newPath` are canonical workspace-relative paths, the same
|
|
524
|
-
* form {@link createFileBackedDocument} stores.
|
|
525
|
-
*
|
|
526
|
-
* A document bound to `oldPath` itself moves, and so does every document nested
|
|
527
|
-
* under it (`oldPath + "/"` prefix), which is what a directory rename produces.
|
|
528
|
-
*
|
|
529
|
-
* The rename on disk has already happened by the time this runs, so it is best
|
|
530
|
-
* effort and never throws: the caller has no way to undo the rename, and a
|
|
531
|
-
* document left on a stale path is recoverable while a failed rename response
|
|
532
|
-
* is not. Rows move one statement at a time so a single row that cannot move
|
|
533
|
-
* does not strand its siblings.
|
|
534
|
-
*
|
|
535
|
-
* A row whose destination is already taken by another binding is one such case:
|
|
536
|
-
* the partial unique index on `workspace_path` rejects the update, and the
|
|
537
|
-
* losing row keeps pointing at the old path. That is deliberate. Overwriting
|
|
538
|
-
* the winner would drop a binding that a client may have open, whereas the
|
|
539
|
-
* stale binding self-heals: opening the file refreshes the row's content from
|
|
540
|
-
* disk, and a document whose file is gone is reported as gone rather than
|
|
541
|
-
* rewritten from the row.
|
|
542
|
-
*/
|
|
543
|
-
export function rebindDocumentsToRenamedPath(params: {
|
|
544
|
-
oldPath: string;
|
|
545
|
-
newPath: string;
|
|
546
|
-
}): void {
|
|
547
|
-
const { oldPath, newPath } = params;
|
|
548
|
-
if (!oldPath || !newPath || oldPath === newPath) {
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
try {
|
|
553
|
-
const rows = rawAll<{ surface_id: string; workspace_path: string }>(
|
|
554
|
-
"documents:rebindRenamedPath:select",
|
|
555
|
-
/*sql*/ `SELECT surface_id, workspace_path
|
|
556
|
-
FROM documents
|
|
557
|
-
WHERE workspace_path = ? OR workspace_path LIKE ? ESCAPE '\\'`,
|
|
558
|
-
oldPath,
|
|
559
|
-
`${escapeSqlLikePattern(oldPath)}/%`,
|
|
560
|
-
);
|
|
561
|
-
|
|
562
|
-
for (const row of rows) {
|
|
563
|
-
const rebound =
|
|
564
|
-
row.workspace_path === oldPath
|
|
565
|
-
? newPath
|
|
566
|
-
: newPath + row.workspace_path.slice(oldPath.length);
|
|
567
|
-
try {
|
|
568
|
-
rawRun(
|
|
569
|
-
"documents:rebindRenamedPath:update",
|
|
570
|
-
/*sql*/ `UPDATE documents SET workspace_path = ? WHERE surface_id = ?`,
|
|
571
|
-
rebound,
|
|
572
|
-
row.surface_id,
|
|
573
|
-
);
|
|
574
|
-
log.info(
|
|
575
|
-
{
|
|
576
|
-
surfaceId: row.surface_id,
|
|
577
|
-
from: row.workspace_path,
|
|
578
|
-
to: rebound,
|
|
579
|
-
},
|
|
580
|
-
"Rebound file-backed document to renamed path",
|
|
581
|
-
);
|
|
582
|
-
} catch (error) {
|
|
583
|
-
log.warn(
|
|
584
|
-
{
|
|
585
|
-
err: error,
|
|
586
|
-
surfaceId: row.surface_id,
|
|
587
|
-
from: row.workspace_path,
|
|
588
|
-
to: rebound,
|
|
589
|
-
},
|
|
590
|
-
"Left file-backed document on its old path after a rename",
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
} catch (error) {
|
|
595
|
-
log.error({ err: error, oldPath, newPath }, "Path rebind error");
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Replace a document's stored content with the current text of the file it is
|
|
601
|
-
* bound to. The file is the source of truth when a document is opened, so this
|
|
602
|
-
* deliberately skips the write-through step — disk and row already agree.
|
|
603
|
-
*/
|
|
604
|
-
export function refreshDocumentContentFromFile(
|
|
605
|
-
surfaceId: string,
|
|
606
|
-
content: string,
|
|
607
|
-
): { success: true } | { success: false; error: string } {
|
|
608
|
-
try {
|
|
609
|
-
rawRun(
|
|
610
|
-
"documents:refreshDocumentContentFromFile",
|
|
611
|
-
/*sql*/ `UPDATE documents SET content = ?, word_count = ?, updated_at = ? WHERE surface_id = ?`,
|
|
612
|
-
content,
|
|
613
|
-
countWords(content),
|
|
614
|
-
Date.now(),
|
|
615
|
-
surfaceId,
|
|
616
|
-
);
|
|
617
|
-
log.info({ surfaceId }, "Refreshed document content from file");
|
|
618
|
-
return { success: true };
|
|
619
|
-
} catch (error) {
|
|
620
|
-
log.error({ err: error, surfaceId }, "Document refresh error");
|
|
621
|
-
return {
|
|
622
|
-
success: false,
|
|
623
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
401
|
// ---------------------------------------------------------------------------
|
|
629
402
|
// Document persistence
|
|
630
403
|
// ---------------------------------------------------------------------------
|
|
@@ -637,7 +410,6 @@ export function saveDocument(params: {
|
|
|
637
410
|
wordCount: number;
|
|
638
411
|
}): { success: true; surfaceId: string } | { success: false; error: string } {
|
|
639
412
|
try {
|
|
640
|
-
writeThroughToWorkspaceFile(params.surfaceId, params.content);
|
|
641
413
|
const now = Date.now();
|
|
642
414
|
rawRun(
|
|
643
415
|
"documents:saveDocument",
|
|
@@ -773,7 +545,6 @@ export function replaceInDocument(
|
|
|
773
545
|
replacementsMade = totalMatches;
|
|
774
546
|
}
|
|
775
547
|
|
|
776
|
-
writeThroughToWorkspaceFile(surfaceId, newContent);
|
|
777
548
|
const wordCount = countWords(newContent);
|
|
778
549
|
rawRun(
|
|
779
550
|
"documents:replaceInDocument:update",
|
|
@@ -940,7 +711,6 @@ export function updateDocumentContent(
|
|
|
940
711
|
const newContent = appending
|
|
941
712
|
? existing.content + sep + appliedMarkdown
|
|
942
713
|
: appliedMarkdown;
|
|
943
|
-
writeThroughToWorkspaceFile(surfaceId, newContent);
|
|
944
714
|
const wordCount = countWords(newContent);
|
|
945
715
|
rawRun(
|
|
946
716
|
"documents:updateDocumentContent:update",
|
package/src/hooks/types.ts
CHANGED
|
@@ -173,6 +173,11 @@ export interface UserPromptSubmitInputContext {
|
|
|
173
173
|
* user speech (e.g. title generation) should skip these turns.
|
|
174
174
|
*/
|
|
175
175
|
readonly isHiddenPrompt?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Daemon-authored kind from the triggering row's `metadata.messageKind`,
|
|
178
|
+
* when the turn needs a more specific identity than role or visibility.
|
|
179
|
+
*/
|
|
180
|
+
readonly messageKind?: string;
|
|
176
181
|
/**
|
|
177
182
|
* The user's original message list, immutable for the hook. Plugins
|
|
178
183
|
* may snapshot or compare against this but MUST NOT mutate it.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { connect, type Socket } from "node:net";
|
|
2
2
|
|
|
3
3
|
import { refreshOverridesFromGateway } from "../config/assistant-feature-flags.js";
|
|
4
|
+
import { getBalancedModelExperimentArm } from "../config/balanced-model-experiment.js";
|
|
4
5
|
import { reconcileFlagGatedProfiles } from "../config/sync-gated-profiles.js";
|
|
5
6
|
import { SYNC_TAGS } from "../daemon/message-types/sync.js";
|
|
6
7
|
import { publishConfigChanged } from "../runtime/sync/resource-sync-events.js";
|
|
@@ -19,13 +20,26 @@ const log = getLogger("gateway-flag-listener");
|
|
|
19
20
|
* when the refresh confirmed flags loaded from the gateway — a transient IPC
|
|
20
21
|
* failure leaves the cache unset and resolves `os-beta` to its registry default
|
|
21
22
|
* `false`, which would remove the user's profile and reset their selection.
|
|
23
|
+
*
|
|
24
|
+
* A new balanced-model experiment arm gets the same broadcast. It repoints what
|
|
25
|
+
* the managed Balanced profile resolves to without moving anything on disk, so
|
|
26
|
+
* the reconcile reports no change and clients would otherwise keep rendering
|
|
27
|
+
* the previous model beside a profile already running the new one.
|
|
22
28
|
*/
|
|
23
29
|
function refreshFlagsAndReconcileProfiles(context: string): void {
|
|
30
|
+
const balancedArmBefore = getBalancedModelExperimentArm();
|
|
24
31
|
refreshOverridesFromGateway()
|
|
25
32
|
.then((loaded) => {
|
|
26
|
-
if (loaded
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
if (!loaded) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const profilesChanged = reconcileFlagGatedProfiles();
|
|
37
|
+
const balancedArmChanged =
|
|
38
|
+
getBalancedModelExperimentArm() !== balancedArmBefore;
|
|
39
|
+
if (profilesChanged || balancedArmChanged) {
|
|
40
|
+
// Reuse the config-changed broadcast clients already consume: web
|
|
41
|
+
// routes the `assistant:self:config` sync tag to the effective profile
|
|
42
|
+
// catalog and the call-site catalog, both derived from config.
|
|
29
43
|
publishConfigChanged();
|
|
30
44
|
}
|
|
31
45
|
})
|
|
@@ -75,11 +75,18 @@ class MockFluxTranscriber implements StreamingTranscriber {
|
|
|
75
75
|
this.received.push(Buffer.from(chunk));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// Transcript Flux answers `CloseStream` with, for a turn still in flight
|
|
79
|
+
// when the caller released. Null models a stop with nothing left to flush.
|
|
80
|
+
pendingFlushText: string | null = null;
|
|
81
|
+
|
|
78
82
|
stop(): void {
|
|
79
83
|
if (this.stopped) {
|
|
80
84
|
return;
|
|
81
85
|
}
|
|
82
86
|
this.stopped = true;
|
|
87
|
+
if (this.pendingFlushText !== null) {
|
|
88
|
+
this.onEvent?.({ type: "final", text: this.pendingFlushText });
|
|
89
|
+
}
|
|
83
90
|
this.onEvent?.({ type: "closed" });
|
|
84
91
|
}
|
|
85
92
|
|
|
@@ -929,4 +936,115 @@ describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
|
|
|
929
936
|
|
|
930
937
|
await session.close("client_end");
|
|
931
938
|
});
|
|
939
|
+
|
|
940
|
+
test("keeps one stream across turns when the provider owns the boundary", async () => {
|
|
941
|
+
const { session, transcribers, turnCalls } = createHarness({
|
|
942
|
+
fluxConfig: FLUX_ON,
|
|
943
|
+
// Long enough that only the provider can be closing these turns.
|
|
944
|
+
silenceThresholdMs: 10_000,
|
|
945
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
await session.start();
|
|
949
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
950
|
+
await waitFor(() => transcribers.length > 0);
|
|
951
|
+
transcribers[0]?.startOfTurn(0);
|
|
952
|
+
transcribers[0]?.endOfTurn("what is the weather", 0);
|
|
953
|
+
await waitFor(() => turnCalls.length === 1);
|
|
954
|
+
await flushAsyncCallbacks();
|
|
955
|
+
|
|
956
|
+
// Second utterance on the same session. A stream torn down per utterance
|
|
957
|
+
// would lose the audio arriving while its replacement dials, which is
|
|
958
|
+
// what eats the opening words of every turn after the first.
|
|
959
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
960
|
+
transcribers[0]?.startOfTurn(1);
|
|
961
|
+
transcribers[0]?.endOfTurn("and tomorrow", 1);
|
|
962
|
+
await waitFor(() => turnCalls.length === 2);
|
|
963
|
+
|
|
964
|
+
expect(transcribers).toHaveLength(1);
|
|
965
|
+
expect(transcribers[0]?.stopped).toBe(false);
|
|
966
|
+
expect(turnCalls[1]?.content).toBe("and tomorrow");
|
|
967
|
+
|
|
968
|
+
await session.close("client_end");
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test("closes the stream when a caller-side boundary releases the turn", async () => {
|
|
972
|
+
const { session, transcribers, turnCalls } = createHarness({
|
|
973
|
+
fluxConfig: FLUX_ON,
|
|
974
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
await session.start();
|
|
978
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
979
|
+
await waitFor(() => transcribers.length > 0);
|
|
980
|
+
// Flux transcribes but never closes the turn, so the fail-open deadline
|
|
981
|
+
// releases it. Nothing sealed this cycle provider-side, so the only way
|
|
982
|
+
// to make Flux answer for the turn still open is to close the stream.
|
|
983
|
+
transcribers[0]?.emit({ type: "partial", text: "are you still there" });
|
|
984
|
+
|
|
985
|
+
await waitFor(
|
|
986
|
+
() => turnCalls.length === 1,
|
|
987
|
+
"Flux fallback never replayed the silence boundary",
|
|
988
|
+
);
|
|
989
|
+
expect(transcribers[0]?.stopped).toBe(true);
|
|
990
|
+
|
|
991
|
+
// The closing stream stays routable: its flush and its close both arrive
|
|
992
|
+
// after stop(), and the cycle is sealed by that close.
|
|
993
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
994
|
+
await waitFor(
|
|
995
|
+
() => transcribers.length === 2,
|
|
996
|
+
"The next utterance never dialed a replacement stream",
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
await session.close("client_end");
|
|
1000
|
+
}, 10_000);
|
|
1001
|
+
|
|
1002
|
+
test("routes a closing stream's flush into the turn it released", async () => {
|
|
1003
|
+
const { session, transcribers, turnCalls } = createHarness({
|
|
1004
|
+
fluxConfig: FLUX_ON,
|
|
1005
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
await session.start();
|
|
1009
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1010
|
+
await waitFor(() => transcribers.length > 0);
|
|
1011
|
+
const transcriber = transcribers[0];
|
|
1012
|
+
// Flux transcribes nothing and never closes the turn, so the fail-open
|
|
1013
|
+
// deadline releases a cycle with no transcript: there is no speculative
|
|
1014
|
+
// dispatch to answer this turn, only what the close flushes.
|
|
1015
|
+
if (transcriber) {
|
|
1016
|
+
transcriber.pendingFlushText = "the whole sentence";
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
await waitFor(
|
|
1020
|
+
() => turnCalls.length === 1,
|
|
1021
|
+
"The flushed transcript never reached an assistant turn",
|
|
1022
|
+
);
|
|
1023
|
+
expect(turnCalls[0]?.content).toBe("the whole sentence");
|
|
1024
|
+
|
|
1025
|
+
await session.close("client_end");
|
|
1026
|
+
}, 10_000);
|
|
1027
|
+
|
|
1028
|
+
test("tears the stream down per utterance while the flag is off", async () => {
|
|
1029
|
+
const { session, transcribers, turnCalls } = createHarness({
|
|
1030
|
+
fluxConfig: { turnEnd: { enabled: false } },
|
|
1031
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
await session.start();
|
|
1035
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1036
|
+
await waitFor(() => transcribers.length > 0);
|
|
1037
|
+
transcribers[0]?.emit({ type: "final", text: "hello there" });
|
|
1038
|
+
|
|
1039
|
+
// With provider turn-end disabled the local silence boundary owns the
|
|
1040
|
+
// turn, and a stream the caller can neither flush nor rely on to seal
|
|
1041
|
+
// itself must close for its transcript to land.
|
|
1042
|
+
await waitFor(
|
|
1043
|
+
() => turnCalls.length === 1,
|
|
1044
|
+
"The silence boundary never committed the turn",
|
|
1045
|
+
);
|
|
1046
|
+
expect(transcribers[0]?.stopped).toBe(true);
|
|
1047
|
+
|
|
1048
|
+
await session.close("client_end");
|
|
1049
|
+
}, 10_000);
|
|
932
1050
|
});
|
|
@@ -15,7 +15,17 @@ import { LiveVoiceSessionManager } from "./live-voice-session-manager.js";
|
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
17
|
|
|
18
|
+
type LiveVoiceSessionFactory =
|
|
19
|
+
typeof import("./live-voice-session.js").createLiveVoiceSession;
|
|
20
|
+
|
|
18
21
|
let manager: LiveVoiceSessionManager | null = null;
|
|
22
|
+
let bundledSessionFactory: LiveVoiceSessionFactory | null = null;
|
|
23
|
+
|
|
24
|
+
export function setBundledLiveVoiceSessionFactory(
|
|
25
|
+
factory: LiveVoiceSessionFactory | null,
|
|
26
|
+
): void {
|
|
27
|
+
bundledSessionFactory = factory;
|
|
28
|
+
}
|
|
19
29
|
|
|
20
30
|
/**
|
|
21
31
|
* The daemon-wide live voice session manager, lazily constructed on first
|
|
@@ -32,9 +42,12 @@ export function getLiveVoiceSessionManager(): LiveVoiceSessionManager {
|
|
|
32
42
|
// module-load time. `require` keeps the factory synchronous, so the
|
|
33
43
|
// manager still claims its single-session slot without an await gap.
|
|
34
44
|
createSession: (context) => {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
const createSession =
|
|
46
|
+
bundledSessionFactory ??
|
|
47
|
+
(
|
|
48
|
+
require("./live-voice-session.js") as typeof import("./live-voice-session.js")
|
|
49
|
+
).createLiveVoiceSession;
|
|
50
|
+
return createSession(context);
|
|
38
51
|
},
|
|
39
52
|
});
|
|
40
53
|
}
|
|
@@ -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
|
});
|