@flowingspring/dsh-voco 0.3.0 → 0.3.1

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/cordis.patch.yml CHANGED
@@ -24,7 +24,8 @@
24
24
  name: '@flowingspring/dsh-voco/voice-assistant'
25
25
  config:
26
26
  maxPendingObservations: 64
27
- restoreConversation: true
27
+ restoreConversation: true
28
+ memoryRecallTimeoutMs: 250
28
29
  maxRestoredUtterances: 24
29
30
  taskSessionPolicy: continuous
30
31
  completedAnnouncement: 任务已完成。
package/lib/client.js CHANGED
@@ -20,9 +20,9 @@ window.__ModuleLoader__.load({
20
20
  var VoiceControl_module_css_default = {
21
21
  "interruptButton": "TFZsua_interruptButton",
22
22
  "active": "TFZsua_active",
23
- "voice-pulse": "TFZsua_voice-pulse",
23
+ "controls": "TFZsua_controls",
24
24
  "button": "TFZsua_button",
25
- "controls": "TFZsua_controls"
25
+ "voice-pulse": "TFZsua_voice-pulse"
26
26
  };
27
27
  //#endregion
28
28
  //#region ../ui-voice/src/client/VoiceControl.tsx
@@ -141,23 +141,23 @@ window.__ModuleLoader__.load({
141
141
  document.head.appendChild(tag);
142
142
  }
143
143
  var VoiceNodeViews_module_css_default = {
144
- "taskLink": "Kt2smW_taskLink",
145
- "taskCard": "Kt2smW_taskCard",
146
- "utterance": "Kt2smW_utterance",
147
144
  "taskDot": "Kt2smW_taskDot",
148
- "bubble": "Kt2smW_bubble",
149
- "voiceBadge": "Kt2smW_voiceBadge",
150
- "taskActions": "Kt2smW_taskActions",
151
- "taskChevron": "Kt2smW_taskChevron",
152
- "taskSummary": "Kt2smW_taskSummary",
153
145
  "miniWave": "Kt2smW_miniWave",
146
+ "taskInput": "Kt2smW_taskInput",
147
+ "taskUpdate": "Kt2smW_taskUpdate",
148
+ "taskCard": "Kt2smW_taskCard",
149
+ "taskTitle": "Kt2smW_taskTitle",
150
+ "taskSummary": "Kt2smW_taskSummary",
151
+ "bubble": "Kt2smW_bubble",
154
152
  "meta": "Kt2smW_meta",
153
+ "taskChevron": "Kt2smW_taskChevron",
155
154
  "taskDetails": "Kt2smW_taskDetails",
156
- "taskUpdate": "Kt2smW_taskUpdate",
155
+ "taskLink": "Kt2smW_taskLink",
156
+ "utterance": "Kt2smW_utterance",
157
157
  "taskCancel": "Kt2smW_taskCancel",
158
- "taskInput": "Kt2smW_taskInput",
159
- "taskStatus": "Kt2smW_taskStatus",
160
- "taskTitle": "Kt2smW_taskTitle"
158
+ "taskActions": "Kt2smW_taskActions",
159
+ "voiceBadge": "Kt2smW_voiceBadge",
160
+ "taskStatus": "Kt2smW_taskStatus"
161
161
  };
162
162
  //#endregion
163
163
  //#region ../ui-voice/src/client/VoiceNodeViews.tsx
@@ -285,11 +285,11 @@ window.__ModuleLoader__.load({
285
285
  document.head.appendChild(tag);
286
286
  }
287
287
  var VoiceOverlay_module_css_default = {
288
- "root": "GPPB2G_root",
289
- "status": "GPPB2G_status",
290
288
  "wave": "GPPB2G_wave",
289
+ "stop": "GPPB2G_stop",
290
+ "root": "GPPB2G_root",
291
291
  "voice-wave": "GPPB2G_voice-wave",
292
- "stop": "GPPB2G_stop"
292
+ "status": "GPPB2G_status"
293
293
  };
294
294
  //#endregion
295
295
  //#region ../ui-voice/src/client/VoiceOverlay.tsx
@@ -129,7 +129,8 @@ const Config = z.object({
129
129
  completedAnnouncement: z.string().default("任务已完成。"),
130
130
  failedAnnouncement: z.string().default("任务失败了,请查看屏幕上的错误信息。"),
131
131
  cancelledAnnouncement: z.string().default("任务已取消。"),
132
- interruptedAnnouncement: z.string().default("上次任务因服务关闭而中断,没有自动重放。你可以告诉我是否继续。")
132
+ interruptedAnnouncement: z.string().default("上次任务因服务关闭而中断,没有自动重放。你可以告诉我是否继续。"),
133
+ memoryRecallTimeoutMs: z.natural().min(1).default(250)
133
134
  });
134
135
  /** Plugin-owned durable session event types, registered with core at load. */
135
136
  const VOICE_SESSION_EVENT_TYPES = [
@@ -166,6 +167,34 @@ function optionalSessionTitle(ctx) {
166
167
  return;
167
168
  }
168
169
  }
170
+ function recallWithSoftTimeout(operation, timeoutMs) {
171
+ const deadline = Math.max(1, timeoutMs);
172
+ return new Promise((resolve) => {
173
+ let settled = false;
174
+ const timer = setTimeout(() => {
175
+ if (settled) return;
176
+ settled = true;
177
+ resolve({ kind: "timeout" });
178
+ }, deadline);
179
+ operation().then((value) => {
180
+ if (settled) return;
181
+ settled = true;
182
+ clearTimeout(timer);
183
+ resolve({
184
+ kind: "resolved",
185
+ value
186
+ });
187
+ }, (error) => {
188
+ if (settled) return;
189
+ settled = true;
190
+ clearTimeout(timer);
191
+ resolve({
192
+ kind: "error",
193
+ error
194
+ });
195
+ });
196
+ });
197
+ }
169
198
  function workspaceMemoryReference(memory) {
170
199
  const sections = [];
171
200
  if (memory.summary.trim() !== "") sections.push(`稳定摘要:\n${memory.summary.trim()}`);
@@ -254,8 +283,10 @@ function apply(ctx, config = {}) {
254
283
  const bindings = /* @__PURE__ */ new Map();
255
284
  const taskBindings = /* @__PURE__ */ new Map();
256
285
  const handles = /* @__PURE__ */ new Map();
286
+ const audioResponsesSeen = /* @__PURE__ */ new Set();
257
287
  const maxPending = config.maxPendingObservations ?? 64;
258
288
  const maxRestoredUtterances = config.maxRestoredUtterances ?? 24;
289
+ const memoryRecallTimeoutMs = config.memoryRecallTimeoutMs ?? 250;
259
290
  const loadConversationMemory = async (sessionId) => {
260
291
  const events = ctx.sessions.get(sessionId)?.events ?? (await ctx.get("sessionPersistence")?.inspect(sessionId))?.events;
261
292
  if (events === void 0) return void 0;
@@ -865,26 +896,69 @@ function apply(ctx, config = {}) {
865
896
  });
866
897
  return;
867
898
  }
899
+ const routeStartedAt = Date.now();
900
+ const input = call.command.input;
901
+ debugVoiceLatency("route-command-received", {
902
+ callId: String(call.id),
903
+ inputLength: input.length
904
+ });
868
905
  let route;
869
906
  try {
870
907
  let memoryReference = "";
871
908
  const memory = optionalWorkspaceMemory(ctx);
872
- if (memory !== void 0) try {
873
- memoryReference = workspaceMemoryReference(await memory.recall({
909
+ if (memory !== void 0) {
910
+ const memoryStartedAt = Date.now();
911
+ debugVoiceLatency("memory-recall-start", { callId: String(call.id) });
912
+ const recallResult = await recallWithSoftTimeout(() => memory.recall({
874
913
  sessionId: binding.sessionId,
875
- query: call.command.input,
914
+ query: input,
876
915
  maxBytes: 5e3
877
- }));
878
- } catch (error) {
879
- ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
916
+ }), memoryRecallTimeoutMs);
917
+ if (recallResult.kind === "resolved") {
918
+ memoryReference = workspaceMemoryReference(recallResult.value);
919
+ debugVoiceLatency("memory-recall-end", {
920
+ callId: String(call.id),
921
+ durationMs: Date.now() - memoryStartedAt,
922
+ referenceLength: memoryReference.length
923
+ });
924
+ } else if (recallResult.kind === "timeout") debugVoiceLatency("memory-recall-timeout", {
925
+ callId: String(call.id),
926
+ durationMs: Date.now() - memoryStartedAt,
927
+ timeoutMs: memoryRecallTimeoutMs,
928
+ fallback: "empty-reference"
929
+ });
930
+ else {
931
+ debugVoiceLatency("memory-recall-error", {
932
+ callId: String(call.id),
933
+ durationMs: Date.now() - memoryStartedAt,
934
+ error: String(recallResult.error)
935
+ });
936
+ ctx.logger.warn(recallResult.error instanceof Error ? recallResult.error : new Error(String(recallResult.error)));
937
+ }
880
938
  }
881
- route = await routeFrontendInput(ctx, requireSourceSession(binding).events, call.command.input, memoryReference);
939
+ route = await routeFrontendInput(ctx, requireSourceSession(binding).events, input, memoryReference);
940
+ debugVoiceLatency("route-decision", {
941
+ callId: String(call.id),
942
+ action: route.action,
943
+ durationMs: Date.now() - routeStartedAt
944
+ });
882
945
  } catch (error) {
946
+ debugVoiceLatency("route-error", {
947
+ callId: String(call.id),
948
+ durationMs: Date.now() - routeStartedAt,
949
+ error: String(error)
950
+ });
883
951
  ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
884
- route = fallbackDelegation(call.command.input, recentConversationText(requireSourceSession(binding).events));
952
+ route = fallbackDelegation(input, recentConversationText(requireSourceSession(binding).events));
885
953
  }
886
954
  if (route.action === "delegate") {
887
955
  const taskId = VoiceTaskId(randomUUID());
956
+ debugVoiceLatency("ack-queued", {
957
+ callId: String(call.id),
958
+ taskId,
959
+ routeDurationMs: Date.now() - routeStartedAt,
960
+ textLength: route.acknowledgement.length
961
+ });
888
962
  speakFragment(binding, taskId, route.acknowledgement);
889
963
  await onTaskCommand(binding, voiceSessionId, {
890
964
  id: call.id,
@@ -915,6 +989,12 @@ function apply(ctx, config = {}) {
915
989
  const taskId = delegationOverride?.taskId ?? VoiceTaskId(randomUUID());
916
990
  const requestText = delegationOverride?.requestText ?? call.command.input;
917
991
  const continuous = (config.taskSessionPolicy ?? "isolated") === "continuous";
992
+ const delegationStartedAt = Date.now();
993
+ debugVoiceLatency("delegation-init-start", {
994
+ callId: String(call.id),
995
+ taskId,
996
+ policy: continuous ? "continuous" : "isolated"
997
+ });
918
998
  let created;
919
999
  try {
920
1000
  if (continuous) created = await ensureContinuousTaskAgent(binding);
@@ -926,7 +1006,19 @@ function apply(ctx, config = {}) {
926
1006
  ...await createTaskAgent(binding, taskSessionId, selection)
927
1007
  };
928
1008
  }
1009
+ debugVoiceLatency("delegation-init-end", {
1010
+ callId: String(call.id),
1011
+ taskId,
1012
+ taskSessionId: created.taskSessionId,
1013
+ durationMs: Date.now() - delegationStartedAt
1014
+ });
929
1015
  } catch (error) {
1016
+ debugVoiceLatency("delegation-init-error", {
1017
+ callId: String(call.id),
1018
+ taskId,
1019
+ durationMs: Date.now() - delegationStartedAt,
1020
+ error: String(error)
1021
+ });
930
1022
  backendUnavailable(error);
931
1023
  return;
932
1024
  }
@@ -1153,6 +1245,11 @@ function apply(ctx, config = {}) {
1153
1245
  });
1154
1246
  return;
1155
1247
  case "transcription.completed":
1248
+ debugVoiceLatency("transcription-completed", {
1249
+ voiceSessionId: String(session.id),
1250
+ utteranceId: String(event.utteranceId),
1251
+ textLength: event.text.length
1252
+ });
1156
1253
  enqueue(binding, async () => {
1157
1254
  endUtterance(binding, event.utteranceId, "user", "completed", event.text);
1158
1255
  if (session.interactionMode === "speech-shell" && binding.voiceSessionId === session.id) await onTranscription(binding, event.text);
@@ -1191,7 +1288,17 @@ function apply(ctx, config = {}) {
1191
1288
  debugVoiceLatency("audio-started");
1192
1289
  return;
1193
1290
  case "output_audio.delta":
1291
+ if (!audioResponsesSeen.has(String(event.responseId))) {
1292
+ audioResponsesSeen.add(String(event.responseId));
1293
+ debugVoiceLatency("audio-first-delta", {
1294
+ responseId: String(event.responseId),
1295
+ audioBytes: event.audio.byteLength
1296
+ });
1297
+ }
1298
+ return;
1194
1299
  case "output_audio.done":
1300
+ audioResponsesSeen.delete(String(event.responseId));
1301
+ return;
1195
1302
  case "task.observation":
1196
1303
  case "error":
1197
1304
  case "closed": return;
@@ -1379,6 +1486,16 @@ async function routeFrontendInput(ctx, events, input, workspaceMemory = "") {
1379
1486
  const recentConversation = recentConversationText(events);
1380
1487
  if (llm === void 0) return fallbackDelegation(input, recentConversation);
1381
1488
  const selection = ctx.agentDefaultModel.currentSelection();
1489
+ const routeLlmStartedAt = Date.now();
1490
+ let routeLlmFirstTextAt;
1491
+ debugVoiceLatency("route-llm-start", {
1492
+ provider: selection.provider,
1493
+ model: selection.model,
1494
+ reasoningEffort: selection.reasoningEffort,
1495
+ inputLength: input.length,
1496
+ recentConversationLength: recentConversation.length,
1497
+ workspaceMemoryLength: workspaceMemory.length
1498
+ });
1382
1499
  const message = createUserMessage({
1383
1500
  content: [{
1384
1501
  type: "text",
@@ -1420,13 +1537,28 @@ async function routeFrontendInput(ctx, events, input, workspaceMemory = "") {
1420
1537
  }
1421
1538
  });
1422
1539
  let output = "";
1423
- for await (const chunk of llm.stream({
1424
- provider: selection.provider,
1425
- model: selection.model,
1426
- ...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort },
1427
- messages: [message],
1428
- system: "你是语音前台路由器。严格按要求输出一个 JSON 对象。不要把普通对话委派给后台编码 Agent。"
1429
- })) if (chunk.type === "text-delta") output += chunk.text;
1540
+ try {
1541
+ for await (const chunk of llm.stream({
1542
+ provider: selection.provider,
1543
+ model: selection.model,
1544
+ ...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort },
1545
+ messages: [message],
1546
+ system: "你是语音前台路由器。严格按要求输出一个 JSON 对象。不要把普通对话委派给后台编码 Agent。"
1547
+ })) {
1548
+ if (chunk.type !== "text-delta") continue;
1549
+ if (routeLlmFirstTextAt === void 0) {
1550
+ routeLlmFirstTextAt = Date.now();
1551
+ debugVoiceLatency("route-llm-first-text", { delayMs: routeLlmFirstTextAt - routeLlmStartedAt });
1552
+ }
1553
+ output += chunk.text;
1554
+ }
1555
+ } finally {
1556
+ debugVoiceLatency("route-llm-end", {
1557
+ durationMs: Date.now() - routeLlmStartedAt,
1558
+ firstTextDelayMs: routeLlmFirstTextAt === void 0 ? void 0 : routeLlmFirstTextAt - routeLlmStartedAt,
1559
+ outputLength: output.length
1560
+ });
1561
+ }
1430
1562
  const normalized = output.trim().replace(/^```(?:json)?\s*/iu, "").replace(/\s*```$/u, "");
1431
1563
  const parsed = JSON.parse(normalized);
1432
1564
  if (parsed.action === "chat" && typeof parsed.reply === "string" && parsed.reply.trim() !== "") return {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flowingspring/dsh-voco",
3
3
  "description": "Persistent voice conversations for DSH with cloud speech recognition, Edge TTS, and background Agent delegation",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",