@turmind/halo 2.2.0 → 2.2.2

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/dist/index.js CHANGED
@@ -1929,7 +1929,7 @@ var init_init = __esm({
1929
1929
  path8.resolve(__dirname, "..", "bundled-docs"),
1930
1930
  path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
1931
1931
  ];
1932
- TEMPLATE_VERSION = 118;
1932
+ TEMPLATE_VERSION = 119;
1933
1933
  VERSION_FILE = ".template-version";
1934
1934
  SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
1935
1935
  BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
@@ -16976,7 +16976,7 @@ async function initObservability(haloProcess = "server") {
16976
16976
  return true;
16977
16977
  }
16978
16978
  const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
16979
- providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.2.0", haloProcess);
16979
+ providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.2.2", haloProcess);
16980
16980
  console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, halo.process=${haloProcess}, capture_content=${config.observability.captureContent})`);
16981
16981
  return true;
16982
16982
  }
@@ -28938,6 +28938,23 @@ var init_handler = __esm({
28938
28938
  });
28939
28939
 
28940
28940
  // ../core/dist/protocol/session-message.js
28941
+ function inferMessageType(msg) {
28942
+ if (msg.type)
28943
+ return msg.type;
28944
+ if (msg.role === "user")
28945
+ return "user";
28946
+ if (msg.role === "assistant")
28947
+ return "assistant";
28948
+ if (msg.toolName)
28949
+ return "tool_call";
28950
+ if (msg.toolOutput !== void 0 && !msg.toolName)
28951
+ return "tool_result";
28952
+ if (msg.usage)
28953
+ return "usage";
28954
+ if (msg.systemPrompt)
28955
+ return "context";
28956
+ return "notification";
28957
+ }
28941
28958
  var init_session_message = __esm({
28942
28959
  "../core/dist/protocol/session-message.js"() {
28943
28960
  "use strict";
@@ -29990,7 +30007,7 @@ function createSttModelManager(opts = {}) {
29990
30007
  }
29991
30008
  };
29992
30009
  }
29993
- var SENSE_VOICE_MODEL, SENSE_VOICE_TOKENS, SILERO_VAD_MODEL, HF_SENSE_VOICE, MS_SENSE_VOICE, STT_MODEL_FILES, sttModels;
30010
+ var SENSE_VOICE_MODEL, SENSE_VOICE_TOKENS, SILERO_VAD_MODEL, PYANNOTE_SEG_MODEL, CAMPLUS_MODEL, HF_SENSE_VOICE, MS_SENSE_VOICE, STT_MODEL_FILES, sttModels;
29994
30011
  var init_models = __esm({
29995
30012
  "../server/dist/stt/models.js"() {
29996
30013
  "use strict";
@@ -29998,6 +30015,8 @@ var init_models = __esm({
29998
30015
  SENSE_VOICE_MODEL = "sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx";
29999
30016
  SENSE_VOICE_TOKENS = "sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt";
30000
30017
  SILERO_VAD_MODEL = "silero-vad-v4/silero_vad.onnx";
30018
+ PYANNOTE_SEG_MODEL = "pyannote-segmentation-3-0/model.int8.onnx";
30019
+ CAMPLUS_MODEL = "campplus-zh-en-advanced/3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx";
30001
30020
  HF_SENSE_VOICE = "https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07";
30002
30021
  MS_SENSE_VOICE = "https://www.modelscope.cn/api/v1/models/pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/repo?Revision=master&FilePath=";
30003
30022
  STT_MODEL_FILES = [
@@ -30027,29 +30046,205 @@ var init_models = __esm({
30027
30046
  { name: "huggingface", url: "https://huggingface.co/csukuangfj/vad/resolve/fba88cd2e921609e7675c3aaf51e0b9b295da4bc/silero_vad.onnx" },
30028
30047
  { name: "modelscope", url: "https://www.modelscope.cn/api/v1/models/pengzhendong/silero-vad/repo?Revision=master&FilePath=v4/silero_vad.onnx" }
30029
30048
  ]
30049
+ },
30050
+ {
30051
+ path: PYANNOTE_SEG_MODEL,
30052
+ size: 1540506,
30053
+ sha256: "d582f4b4c6b48205de7e0643c57df0df5615a3c176189be3fc461e9d18827b5d",
30054
+ sources: [
30055
+ { name: "huggingface", url: "https://huggingface.co/csukuangfj/sherpa-onnx-pyannote-segmentation-3-0/resolve/9403a6902bb58e3d5ae8c7e77c3422de279db2e0/model.int8.onnx" },
30056
+ { name: "modelscope", url: "https://www.modelscope.cn/api/v1/models/pengzhendong/sherpa-onnx-pyannote-segmentation-3-0/repo?Revision=master&FilePath=model.int8.onnx" }
30057
+ ]
30058
+ },
30059
+ {
30060
+ path: CAMPLUS_MODEL,
30061
+ size: 28281164,
30062
+ sha256: "aa3cfc16963a10586a9393f5035d6d6b57e98d358b347f80c2a30bf4f00ceba2",
30063
+ sources: [
30064
+ { name: "huggingface", url: "https://huggingface.co/csukuangfj/speaker-embedding-models/resolve/0743f301363dec56491a490f6d6cbc9d67f9a3bf/3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx" },
30065
+ { name: "modelscope", url: "https://www.modelscope.cn/api/v1/models/lijiu09/speaker-embedding-models/repo?Revision=master&FilePath=3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx" },
30066
+ { name: "github", url: "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx" }
30067
+ ]
30030
30068
  }
30031
30069
  ];
30032
30070
  sttModels = createSttModelManager();
30033
30071
  }
30034
30072
  });
30035
30073
 
30074
+ // ../server/dist/stt/speakers.js
30075
+ function normalize(v) {
30076
+ let n = 0;
30077
+ for (const x of v)
30078
+ n += x * x;
30079
+ n = Math.sqrt(n) || 1;
30080
+ return v.map((x) => x / n);
30081
+ }
30082
+ function cosine(a, b) {
30083
+ let d = 0, x = 0, y = 0;
30084
+ for (let i = 0; i < a.length; i++) {
30085
+ d += a[i] * b[i];
30086
+ x += a[i] * a[i];
30087
+ y += b[i] * b[i];
30088
+ }
30089
+ return x && y ? d / Math.sqrt(x * y) : 0;
30090
+ }
30091
+ function createSpeakerTracker(threshold = SPEAKER_MATCH_THRESHOLD) {
30092
+ const centroids = [];
30093
+ return {
30094
+ /** One local cluster (its embedding + seconds of audio) → speaker index. */
30095
+ assign(embedding, seconds) {
30096
+ const e = normalize(embedding);
30097
+ let best = -1;
30098
+ let bestSim = -2;
30099
+ centroids.forEach((c, k) => {
30100
+ const s = cosine(c, e);
30101
+ if (s > bestSim) {
30102
+ bestSim = s;
30103
+ best = k;
30104
+ }
30105
+ });
30106
+ if (best < 0 || bestSim < threshold && seconds >= MIN_NEW_SPEAKER_S) {
30107
+ centroids.push(e);
30108
+ return centroids.length - 1;
30109
+ }
30110
+ if (bestSim >= threshold) {
30111
+ const c = centroids[best];
30112
+ for (let i = 0; i < c.length; i++)
30113
+ c[i] += e[i];
30114
+ }
30115
+ return best;
30116
+ }
30117
+ };
30118
+ }
30119
+ function runAt(runs, t2) {
30120
+ let hit = null;
30121
+ for (const r of runs)
30122
+ if (r.start <= t2 && t2 <= r.end && (!hit || r.start > hit.start))
30123
+ hit = r;
30124
+ if (hit)
30125
+ return hit;
30126
+ let best = runs[0];
30127
+ let dist = Infinity;
30128
+ for (const r of runs) {
30129
+ const d = Math.min(Math.abs(t2 - r.start), Math.abs(t2 - r.end));
30130
+ if (d < dist) {
30131
+ dist = d;
30132
+ best = r;
30133
+ }
30134
+ }
30135
+ return best;
30136
+ }
30137
+ function splitBySpeakerRuns(text6, tokens, timestamps, runs, duration) {
30138
+ const audio = /* @__PURE__ */ new Map();
30139
+ for (const r of runs)
30140
+ audio.set(r.speaker, (audio.get(r.speaker) ?? 0) + r.end - r.start);
30141
+ const top = [...audio.entries()].reduce((a, b) => b[1] > a[1] ? b : a)[0];
30142
+ const whole = [{ start: 0, end: duration, text: text6, speaker: top }];
30143
+ if (audio.size === 1 || !tokens || !timestamps || tokens.length !== timestamps.length || tokens.join("").trim() !== text6)
30144
+ return whole;
30145
+ const pieces = [];
30146
+ for (let i = 0; i < tokens.length; i++) {
30147
+ const cur = pieces.at(-1);
30148
+ if (!WORD.test(tokens[i])) {
30149
+ if (cur)
30150
+ cur.last = i;
30151
+ continue;
30152
+ }
30153
+ const run = runAt(runs, timestamps[i]);
30154
+ if (cur && cur.speaker === run.speaker) {
30155
+ cur.last = i;
30156
+ continue;
30157
+ }
30158
+ pieces.push(cur ? { speaker: run.speaker, first: i, last: i, at: Math.min(Math.max(run.start, timestamps[cur.last]), timestamps[i]) } : { speaker: run.speaker, first: 0, last: i, at: 0 });
30159
+ }
30160
+ for (let k = pieces.length - 1; k >= 0 && pieces.length > 1; k--) {
30161
+ if ((pieces[k + 1]?.at ?? duration) > pieces[k].at)
30162
+ continue;
30163
+ if (k > 0)
30164
+ pieces[k - 1].last = pieces[k].last;
30165
+ else
30166
+ Object.assign(pieces[1], { first: pieces[0].first, at: 0 });
30167
+ pieces.splice(k, 1);
30168
+ }
30169
+ for (let k = pieces.length - 1; k > 0; k--) {
30170
+ if (pieces[k].speaker !== pieces[k - 1].speaker)
30171
+ continue;
30172
+ pieces[k - 1].last = pieces[k].last;
30173
+ pieces.splice(k, 1);
30174
+ }
30175
+ if (pieces.length === 0)
30176
+ return whole;
30177
+ return pieces.map((p, k) => ({
30178
+ start: p.at,
30179
+ end: pieces[k + 1]?.at ?? duration,
30180
+ text: tokens.slice(p.first, p.last + 1).join("").trim(),
30181
+ speaker: p.speaker
30182
+ }));
30183
+ }
30184
+ var SPEAKER_MATCH_THRESHOLD, MIN_NEW_SPEAKER_S, WORD;
30185
+ var init_speakers = __esm({
30186
+ "../server/dist/stt/speakers.js"() {
30187
+ "use strict";
30188
+ SPEAKER_MATCH_THRESHOLD = 0.55;
30189
+ MIN_NEW_SPEAKER_S = 1;
30190
+ WORD = /[\p{L}\p{N}]/u;
30191
+ }
30192
+ });
30193
+
30036
30194
  // ../server/dist/stt/engine.js
30037
30195
  import os8 from "node:os";
30196
+ function withTimeout(p, ms) {
30197
+ let timer2;
30198
+ const timeout = new Promise((_, reject) => {
30199
+ timer2 = setTimeout(() => reject(new Error("timed out")), ms);
30200
+ });
30201
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer2));
30202
+ }
30038
30203
  function createSttEngine(deps = {}) {
30039
30204
  const loadSherpa = deps.loadSherpa ?? defaultLoadSherpa;
30040
30205
  const models = deps.models ?? sttModels;
30041
30206
  let sherpa = null;
30042
30207
  let recognizer = null;
30043
30208
  let loading = null;
30209
+ let diarizer = null;
30210
+ let extractor = null;
30211
+ let diarLoading = null;
30044
30212
  const queue = [];
30045
30213
  let decoding = false;
30046
- function load2() {
30047
- if (recognizer)
30048
- return Promise.resolve();
30049
- loading ??= doLoad().finally(() => {
30050
- loading = null;
30051
- });
30052
- return loading;
30214
+ async function load2(opts = {}) {
30215
+ if (!recognizer)
30216
+ await (loading ??= doLoad().finally(() => {
30217
+ loading = null;
30218
+ }));
30219
+ if (opts.speakers && !diarizer)
30220
+ await (diarLoading ??= doLoadDiarizer().finally(() => {
30221
+ diarLoading = null;
30222
+ }));
30223
+ }
30224
+ async function doLoadDiarizer() {
30225
+ const t0 = Date.now();
30226
+ try {
30227
+ const embedding = { model: models.filePath(CAMPLUS_MODEL), numThreads: NUM_THREADS, provider: "cpu", debug: 0 };
30228
+ const d = new sherpa.OfflineSpeakerDiarization({
30229
+ segmentation: { pyannote: { model: models.filePath(PYANNOTE_SEG_MODEL) }, numThreads: NUM_THREADS, provider: "cpu", debug: 0 },
30230
+ embedding,
30231
+ clustering: { numClusters: -1, threshold: DIAR_CLUSTER_THRESHOLD },
30232
+ minDurationOn: DIAR_MIN_ON_S,
30233
+ minDurationOff: DIAR_MIN_OFF_S
30234
+ });
30235
+ extractor = new sherpa.SpeakerEmbeddingExtractor(embedding);
30236
+ diarizer = d;
30237
+ console.log(`[STT] speaker diarizer loaded in ${Date.now() - t0}ms`);
30238
+ } catch (err) {
30239
+ console.log(`[STT] speaker diarizer load failed: ${errMsg2(err)}`);
30240
+ throw new SttError("engine", `speaker labelling unavailable: ${errMsg2(err)}`);
30241
+ }
30242
+ }
30243
+ function embed(samples) {
30244
+ const s = extractor.createStream();
30245
+ s.acceptWaveform({ samples: samples.subarray(0, EMBED_MAX_S * SAMPLE_RATE), sampleRate: SAMPLE_RATE });
30246
+ s.inputFinished();
30247
+ return extractor.compute(s, false);
30053
30248
  }
30054
30249
  async function doLoad() {
30055
30250
  const t0 = Date.now();
@@ -30079,20 +30274,32 @@ function createSttEngine(deps = {}) {
30079
30274
  decoding = true;
30080
30275
  while (queue.length > 0) {
30081
30276
  const job = queue.shift();
30277
+ let phase = "decode";
30082
30278
  try {
30083
30279
  const stream = recognizer.createStream();
30084
30280
  stream.acceptWaveform({ samples: job.samples, sampleRate: SAMPLE_RATE });
30085
30281
  await recognizer.decodeAsync(stream);
30086
- job.owner.done(job, recognizer.getResult(stream));
30282
+ const result = recognizer.getResult(stream);
30283
+ let diar = null;
30284
+ if (job.owner.wantsSpeakers() && (result.text ?? "").trim()) {
30285
+ phase = "speaker diarization";
30286
+ diar = await withTimeout(sherpa.offlineSpeakerDiarizationProcessAsync(diarizer.handle, job.samples, () => {
30287
+ }), DIAR_TIMEOUT_MS);
30288
+ }
30289
+ await job.owner.done(job, result, diar);
30087
30290
  } catch (err) {
30088
- job.owner.fail(new SttError("engine", `decode failed: ${errMsg2(err)}`));
30291
+ job.owner.fail(new SttError("engine", `${phase} failed: ${errMsg2(err)}`));
30089
30292
  }
30090
30293
  }
30091
30294
  decoding = false;
30092
30295
  }
30093
- function open(handlers) {
30296
+ function open(handlers, opts = {}) {
30094
30297
  if (!sherpa)
30095
30298
  throw new SttError("engine", "speech engine not loaded");
30299
+ if (opts.speakers && !diarizer)
30300
+ throw new SttError("engine", "speaker labelling not loaded");
30301
+ const speakers = opts.speakers ? createSpeakerTracker() : null;
30302
+ const labels = /* @__PURE__ */ new Map();
30096
30303
  let vad;
30097
30304
  try {
30098
30305
  vad = new sherpa.Vad({
@@ -30148,16 +30355,67 @@ function createSttEngine(deps = {}) {
30148
30355
  handlers.onError(err);
30149
30356
  checkDrain();
30150
30357
  }
30358
+ async function speakerRuns(samples, diar) {
30359
+ const duration = samples.length / SAMPLE_RATE;
30360
+ const tick = async () => {
30361
+ await new Promise((r) => setImmediate(r));
30362
+ return !stopped;
30363
+ };
30364
+ if (diar.length === 0)
30365
+ return await tick() ? [{ start: 0, end: duration, speaker: speakers.assign(embed(samples), duration) }] : null;
30366
+ const clusters = /* @__PURE__ */ new Map();
30367
+ for (const d of [...diar].sort((a, b) => a.start - b.start))
30368
+ clusters.set(d.speaker, [...clusters.get(d.speaker) ?? [], d]);
30369
+ const seconds = (segs) => segs.reduce((n, d) => n + d.end - d.start, 0);
30370
+ const runs = [];
30371
+ for (const segs of [...clusters.values()].sort((a, b) => seconds(b) - seconds(a))) {
30372
+ const parts = segs.map((d) => samples.subarray(Math.round(d.start * SAMPLE_RATE), Math.round(d.end * SAMPLE_RATE)));
30373
+ const audio = new Float32Array(Math.min(parts.reduce((n, p) => n + p.length, 0), EMBED_MAX_S * SAMPLE_RATE));
30374
+ let at = 0;
30375
+ for (const p of parts) {
30376
+ if (at >= audio.length)
30377
+ break;
30378
+ audio.set(p.subarray(0, audio.length - at), at);
30379
+ at += p.length;
30380
+ }
30381
+ if (!await tick())
30382
+ return null;
30383
+ const speaker = speakers.assign(embed(audio), seconds(segs));
30384
+ for (const d of segs)
30385
+ runs.push({ start: d.start, end: d.end, speaker });
30386
+ }
30387
+ return runs;
30388
+ }
30151
30389
  const owner = {
30152
- done(job, result) {
30390
+ wantsSpeakers: () => speakers !== null && !stopped,
30391
+ async done(job, result, diar) {
30392
+ const text6 = (result.text ?? "").trim();
30393
+ let pieces = null;
30394
+ if (text6 && diar && !stopped) {
30395
+ try {
30396
+ const runs = await speakerRuns(job.samples, diar);
30397
+ if (runs)
30398
+ pieces = splitBySpeakerRuns(text6, result.tokens, result.timestamps, runs, job.samples.length / SAMPLE_RATE);
30399
+ } catch (err) {
30400
+ failStream(new SttError("engine", `speaker labelling failed: ${errMsg2(err)}`));
30401
+ }
30402
+ }
30153
30403
  pendingSamples -= job.samples.length;
30154
30404
  inFlight2--;
30155
30405
  if (!stopped) {
30156
30406
  segments++;
30157
30407
  decodedSamples += job.samples.length;
30158
- const text6 = (result.text ?? "").trim();
30159
- if (text6)
30160
- handlers.onFinal({ start: round3(job.start / SAMPLE_RATE), end: round3(job.end / SAMPLE_RATE), text: text6, lang: (result.lang ?? "").replace(/<\|([^|]*)\|>/g, "$1") });
30408
+ const lang = (result.lang ?? "").replace(/<\|([^|]*)\|>/g, "$1");
30409
+ const start3 = job.start / SAMPLE_RATE;
30410
+ const end = job.end / SAMPLE_RATE;
30411
+ if (pieces) {
30412
+ for (const p of pieces) {
30413
+ if (!labels.has(p.speaker))
30414
+ labels.set(p.speaker, String(labels.size));
30415
+ handlers.onFinal({ start: round3(start3 + p.start), end: round3(Math.min(start3 + p.end, end)), text: p.text, lang, speaker: labels.get(p.speaker) });
30416
+ }
30417
+ } else if (text6)
30418
+ handlers.onFinal({ start: round3(start3), end: round3(end), text: text6, lang });
30161
30419
  }
30162
30420
  checkDrain();
30163
30421
  },
@@ -30246,16 +30504,17 @@ function createSttEngine(deps = {}) {
30246
30504
  dropQueued();
30247
30505
  checkDrain();
30248
30506
  },
30249
- stats: () => ({ segments, audioSeconds: round3(decodedSamples / SAMPLE_RATE) })
30507
+ stats: () => ({ segments, audioSeconds: round3(decodedSamples / SAMPLE_RATE), ...speakers ? { speakers: labels.size } : {} })
30250
30508
  };
30251
30509
  }
30252
30510
  return { load: load2, open };
30253
30511
  }
30254
- var SAMPLE_RATE, VAD_WINDOW, VAD_THRESHOLD, VAD_MIN_SILENCE_S, VAD_MIN_SPEECH_S, VAD_MAX_SPEECH_S, PAD_BEFORE_S, PAD_AFTER_S, RING_S, MAX_QUEUED_SEGMENTS, MAX_PENDING_AUDIO_S, NUM_THREADS, SttError, defaultLoadSherpa, errMsg2, round3, sttEngine;
30512
+ var SAMPLE_RATE, VAD_WINDOW, VAD_THRESHOLD, VAD_MIN_SILENCE_S, VAD_MIN_SPEECH_S, VAD_MAX_SPEECH_S, PAD_BEFORE_S, PAD_AFTER_S, RING_S, MAX_QUEUED_SEGMENTS, MAX_PENDING_AUDIO_S, NUM_THREADS, DIAR_CLUSTER_THRESHOLD, DIAR_MIN_ON_S, DIAR_MIN_OFF_S, EMBED_MAX_S, DIAR_TIMEOUT_MS, SttError, defaultLoadSherpa, errMsg2, round3, sttEngine;
30255
30513
  var init_engine = __esm({
30256
30514
  "../server/dist/stt/engine.js"() {
30257
30515
  "use strict";
30258
30516
  init_models();
30517
+ init_speakers();
30259
30518
  SAMPLE_RATE = 16e3;
30260
30519
  VAD_WINDOW = 512;
30261
30520
  VAD_THRESHOLD = 0.5;
@@ -30268,6 +30527,11 @@ var init_engine = __esm({
30268
30527
  MAX_QUEUED_SEGMENTS = 32;
30269
30528
  MAX_PENDING_AUDIO_S = 60;
30270
30529
  NUM_THREADS = Math.min(4, Math.max(1, Math.floor(os8.availableParallelism() / 2)));
30530
+ DIAR_CLUSTER_THRESHOLD = 0.6;
30531
+ DIAR_MIN_ON_S = 0.3;
30532
+ DIAR_MIN_OFF_S = 0.5;
30533
+ EMBED_MAX_S = 6;
30534
+ DIAR_TIMEOUT_MS = 6e4;
30271
30535
  SttError = class extends Error {
30272
30536
  code;
30273
30537
  constructor(code, message) {
@@ -30276,7 +30540,10 @@ var init_engine = __esm({
30276
30540
  this.name = "SttError";
30277
30541
  }
30278
30542
  };
30279
- defaultLoadSherpa = async () => (await import("sherpa-onnx-node")).default;
30543
+ defaultLoadSherpa = async () => {
30544
+ const [m, addon] = await Promise.all([import("sherpa-onnx-node"), import("sherpa-onnx-node/addon.js")]);
30545
+ return { ...m.default, offlineSpeakerDiarizationProcessAsync: addon.default.offlineSpeakerDiarizationProcessAsync };
30546
+ };
30280
30547
  errMsg2 = (err) => err instanceof Error ? err.message : String(err);
30281
30548
  round3 = (s) => Math.round(s * 1e3) / 1e3;
30282
30549
  sttEngine = createSttEngine();
@@ -30308,7 +30575,9 @@ function createSttProxy(deps = {}) {
30308
30575
  }
30309
30576
  });
30310
30577
  wss2.on("connection", (ws, req) => {
30311
- const extId = parseQuery2(req).get("ext");
30578
+ const q = parseQuery2(req);
30579
+ const extId = q.get("ext");
30580
+ const speakers = q.get("speakers") === "1";
30312
30581
  const openedAt = Date.now();
30313
30582
  let closed = false;
30314
30583
  let failed = false;
@@ -30332,14 +30601,14 @@ function createSttProxy(deps = {}) {
30332
30601
  };
30333
30602
  async function startEngine() {
30334
30603
  try {
30335
- await engine.load();
30604
+ await engine.load({ speakers });
30336
30605
  if (closed || failed || ended)
30337
30606
  return;
30338
30607
  stream = engine.open({
30339
30608
  // Recognized text goes to the client only — never to the log.
30340
30609
  onFinal: (f) => send2({ type: "final", ...f }),
30341
30610
  onError: (err) => fail2(err.code, err.message)
30342
- });
30611
+ }, { speakers });
30343
30612
  } catch (err) {
30344
30613
  if (err instanceof SttError)
30345
30614
  fail2(err.code, err.message);
@@ -30364,7 +30633,7 @@ function createSttProxy(deps = {}) {
30364
30633
  else if (s.state === "error")
30365
30634
  fail2("model", s.message ?? "model download failed");
30366
30635
  };
30367
- console.log(`[STT] stream open ext=${extId}`);
30636
+ console.log(`[STT] stream open ext=${extId}${speakers ? " speakers=1" : ""}`);
30368
30637
  ws.on("message", (data, isBinary) => {
30369
30638
  if (ended || failed)
30370
30639
  return;
@@ -30400,7 +30669,7 @@ function createSttProxy(deps = {}) {
30400
30669
  unsubscribe = null;
30401
30670
  stream?.abort();
30402
30671
  const st = stream?.stats();
30403
- console.log(`[STT] stream closed after ${((Date.now() - openedAt) / 1e3).toFixed(1)}s ext=${extId}${st ? ` segments=${st.segments} audio=${st.audioSeconds.toFixed(1)}s` : ""}`);
30672
+ console.log(`[STT] stream closed after ${((Date.now() - openedAt) / 1e3).toFixed(1)}s ext=${extId}${st ? ` segments=${st.segments} audio=${st.audioSeconds.toFixed(1)}s${st.speakers !== void 0 ? ` speakers=${st.speakers}` : ""}` : ""}`);
30404
30673
  });
30405
30674
  const initial = models.status();
30406
30675
  if (initial.state === "ready") {
@@ -31566,6 +31835,12 @@ var init_terminal_manager = __esm({
31566
31835
 
31567
31836
  // ../server/dist/ws/handler.js
31568
31837
  import path78 from "node:path";
31838
+ function adminSnapshotRows(rows) {
31839
+ return rows.filter((m) => {
31840
+ const t2 = inferMessageType(m);
31841
+ return t2 !== "tool_call" && t2 !== "tool_result";
31842
+ });
31843
+ }
31569
31844
  function setupWebSocketHandler(deps) {
31570
31845
  const { wss: wss2, registry: registry2 } = deps;
31571
31846
  const clients = /* @__PURE__ */ new Set();
@@ -31689,8 +31964,10 @@ function setupWebSocketHandler(deps) {
31689
31964
  }
31690
31965
  if (client.subscriptions.has(targetSessionId)) {
31691
31966
  const state = getState(client, targetSessionId);
31692
- const messages2 = state ? [...createSaveSnapshot(state)] : [];
31693
- sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId: targetSessionId } });
31967
+ const log2 = state?.messageLog ?? [];
31968
+ const deletedIds = log2.filter((m) => m.deleted).map((m) => m.id);
31969
+ const userRows = log2.filter((m) => m.role === "user" && !m.taskId).length;
31970
+ sendJson(ws, { type: "exchange:deleted", sessionId: targetSessionId, deletedIds, userOrdinal: msg.userOrdinal, userRows });
31694
31971
  }
31695
31972
  }
31696
31973
  function createEventListener(client, sessionId) {
@@ -32074,7 +32351,7 @@ ${msg.message}` : markers.join("\n");
32074
32351
  const state2 = getState(client2, sid);
32075
32352
  const ctxConfig = await sm.getContextConfig(sid);
32076
32353
  const running = sm.isSessionRunning(sid);
32077
- const messages3 = state2 ? running ? [...state2.messageLog] : [...createSaveSnapshot(state2)] : [];
32354
+ const messages3 = state2 ? adminSnapshotRows(running ? state2.messageLog : createSaveSnapshot(state2)) : [];
32078
32355
  const detachedSession = sm.getSessionById(sid);
32079
32356
  sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages3, sessionId: sid, maxContextTokens: ctxConfig.maxTokens, agentId: detachedSession?.agentId, archiveCount: archiveCountFor(client2, sid, detachedSession?.agentId), accessLevel: detachedSession?.accessLevel ?? null } });
32080
32357
  if (state2 && state2.contextTokens > 0) {
@@ -32131,7 +32408,7 @@ ${msg.message}` : markers.join("\n");
32131
32408
  }
32132
32409
  }
32133
32410
  const state = sid ? getState(client2, sid) : null;
32134
- const messages2 = state ? [...createSaveSnapshot(state)] : [];
32411
+ const messages2 = state ? adminSnapshotRows(createSaveSnapshot(state)) : [];
32135
32412
  sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId: sid, maxContextTokens, agentId, archiveCount: sid ? archiveCountFor(client2, sid, agentId) : 0, accessLevel } });
32136
32413
  if (state && state.contextTokens > 0) {
32137
32414
  sendJson(ws, { type: "chat:usage", contextTokens: state.contextTokens, outputTokens: state.outputTokens, sessionId: sid });
@@ -32161,6 +32438,7 @@ var CLIENT_SILENCE_LIMIT_MS, detachedSessions;
32161
32438
  var init_handler2 = __esm({
32162
32439
  "../server/dist/ws/handler.js"() {
32163
32440
  "use strict";
32441
+ init_session_types();
32164
32442
  init_sandbox();
32165
32443
  init_ui_log_builder();
32166
32444
  init_config();
@@ -32291,7 +32569,7 @@ function buildCard(workspace, interfaceUrl, caps) {
32291
32569
  return {
32292
32570
  name: raw.name,
32293
32571
  description: raw.description,
32294
- version: typeof raw.version === "string" ? raw.version : "2.2.0",
32572
+ version: typeof raw.version === "string" ? raw.version : "2.2.2",
32295
32573
  supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
32296
32574
  capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
32297
32575
  ...caps.tokenAuth === false ? {} : {
@@ -34046,6 +34324,8 @@ async function runJob(jobId, triggerKind) {
34046
34324
  kickPendingEvents(jobId);
34047
34325
  return runId;
34048
34326
  };
34327
+ if (triggerKind === "manual" && !job.userPrompt)
34328
+ return skip("nothing to run: the job has no prompt, and run-now carries no webhook events");
34049
34329
  if (_inflight.has(jobId))
34050
34330
  return skip("previous run still in progress");
34051
34331
  if (_inflightSessions.has(sessionKeyFor(job.workspacePath, sessionId)))
@@ -34131,9 +34411,9 @@ async function runJob(jobId, triggerKind) {
34131
34411
  const events = webhook ? claimPendingEvents(jobId, runId, sessionId) : [];
34132
34412
  try {
34133
34413
  const texts = events.map((ev) => eventText(ev, job.workspacePath, false));
34134
- const stamp = host.appendUserMessage(sessionId, [job.userPrompt, ...texts.map((t2) => t2.ui)].join("\n\n"));
34414
+ const stamp = host.appendUserMessage(sessionId, [job.userPrompt, ...texts.map((t2) => t2.ui)].filter(Boolean).join("\n\n"));
34135
34415
  stampEvents(events.map((ev) => ev.id), stamp);
34136
- await host.sendUserMessage(sessionId, [UNATTENDED_PREAMBLE2, job.userPrompt, ...texts.map((t2) => t2.model)].join("\n\n"), void 0, void 0, stamp);
34416
+ await host.sendUserMessage(sessionId, [UNATTENDED_PREAMBLE2, job.userPrompt, ...texts.map((t2) => t2.model)].filter(Boolean).join("\n\n"), void 0, void 0, stamp);
34137
34417
  } catch (err) {
34138
34418
  untrackRun(runId);
34139
34419
  tracked = false;
@@ -34531,7 +34811,7 @@ function createCronRoutes() {
34531
34811
  const body = await c.req.json().catch(() => null);
34532
34812
  if (!body)
34533
34813
  return c.json({ error: "invalid body" }, 400);
34534
- for (const k of ["workspacePath", "agentId", "userPrompt"]) {
34814
+ for (const k of ["workspacePath", "agentId"]) {
34535
34815
  if (!body[k] || typeof body[k] !== "string")
34536
34816
  return c.json({ error: `${k} required` }, 400);
34537
34817
  }
@@ -34540,6 +34820,11 @@ function createCronRoutes() {
34540
34820
  const hasSchedule = typeof body.schedule === "string" && body.schedule.trim().length > 0;
34541
34821
  const hasRunAt = typeof body.runAt === "number" && Number.isFinite(body.runAt);
34542
34822
  const webhook = body.webhook === true;
34823
+ const userPrompt = body.userPrompt ?? "";
34824
+ if (typeof userPrompt !== "string")
34825
+ return c.json({ error: "userPrompt must be a string" }, 400);
34826
+ if (!userPrompt && !webhook)
34827
+ return c.json({ error: "userPrompt required" }, 400);
34543
34828
  if (!hasSchedule && !hasRunAt && !webhook)
34544
34829
  return c.json({ error: "schedule, runAt or webhook required" }, 400);
34545
34830
  if (hasSchedule && hasRunAt)
@@ -34570,8 +34855,6 @@ function createCronRoutes() {
34570
34855
  const stateless = body.stateless === true;
34571
34856
  if (stateless && session.sessionId)
34572
34857
  return c.json({ error: STATELESS_SESSION_ERR }, 400);
34573
- if (webhook && session.sessionId)
34574
- return c.json({ error: WEBHOOK_SESSION_ERR }, 400);
34575
34858
  const folder = await parseWorkingDir(body.workspacePath, body.workingDir);
34576
34859
  if ("error" in folder)
34577
34860
  return c.json({ error: folder.error }, 400);
@@ -34598,7 +34881,7 @@ function createCronRoutes() {
34598
34881
  label: body.label && body.label.trim().length > 0 ? body.label.trim() : null,
34599
34882
  workspacePath: body.workspacePath,
34600
34883
  agentId: body.agentId,
34601
- userPrompt: body.userPrompt,
34884
+ userPrompt,
34602
34885
  schedule: hasSchedule ? body.schedule : "",
34603
34886
  runAt: hasRunAt ? body.runAt : null,
34604
34887
  webhookSecret: webhook ? newWebhookSecret() : null,
@@ -34694,8 +34977,12 @@ function createCronRoutes() {
34694
34977
  if (!nextWebhookSecret && nextSchedule.trim().length === 0 && typeof nextRunAt !== "number") {
34695
34978
  return c.json({ error: "schedule, runAt or webhook required" }, 400);
34696
34979
  }
34697
- if (nextWebhookSecret && merged.sessionId)
34698
- return c.json({ error: WEBHOOK_SESSION_ERR }, 400);
34980
+ if (body.userPrompt !== void 0 && typeof body.userPrompt !== "string")
34981
+ return c.json({ error: "userPrompt must be a string" }, 400);
34982
+ const nextPrompt = body.userPrompt ?? existing.userPrompt;
34983
+ if (!nextWebhookSecret && !nextPrompt && (body.userPrompt !== void 0 || existing.webhookSecret !== null)) {
34984
+ return c.json({ error: "userPrompt required (only a webhook job may leave it empty)" }, 400);
34985
+ }
34699
34986
  for (const k of ["activeFrom", "activeUntil"]) {
34700
34987
  const err = validateActiveBound(k, body[k]);
34701
34988
  if (err)
@@ -34720,7 +35007,7 @@ function createCronRoutes() {
34720
35007
  if (body.agentId !== void 0)
34721
35008
  patch.agentId = body.agentId;
34722
35009
  if (body.userPrompt !== void 0)
34723
- patch.userPrompt = body.userPrompt;
35010
+ patch.userPrompt = nextPrompt;
34724
35011
  if (body.schedule !== void 0 || clearSchedule)
34725
35012
  patch.schedule = nextSchedule;
34726
35013
  if (body.runAt !== void 0 || clearRunAt)
@@ -34871,7 +35158,7 @@ function computeNextRun(j) {
34871
35158
  return null;
34872
35159
  }
34873
35160
  }
34874
- var WEBHOOK_SESSION_ERR, TIMEOUT_SEC_MIN, TIMEOUT_SEC_MAX, STATELESS_SESSION_ERR;
35161
+ var TIMEOUT_SEC_MIN, TIMEOUT_SEC_MAX, STATELESS_SESSION_ERR;
34875
35162
  var init_cron = __esm({
34876
35163
  "../server/dist/routes/cron.js"() {
34877
35164
  "use strict";
@@ -34881,7 +35168,6 @@ var init_cron = __esm({
34881
35168
  init_dispatcher();
34882
35169
  init_broadcast();
34883
35170
  init_session_id();
34884
- WEBHOOK_SESSION_ERR = "a webhook job runs in its own session (or a new one every run), not a picked sessionId";
34885
35171
  TIMEOUT_SEC_MIN = 60;
34886
35172
  TIMEOUT_SEC_MAX = 21600;
34887
35173
  STATELESS_SESSION_ERR = "stateless (new session every run) and sessionId are mutually exclusive";
@@ -35381,7 +35667,7 @@ function createCoreChecker(opts) {
35381
35667
  }
35382
35668
  function getCoreStatus(force) {
35383
35669
  checker ??= createCoreChecker({
35384
- haloVersion: "2.2.0",
35670
+ haloVersion: "2.2.2",
35385
35671
  fetchLatest: fetchNpmLatest,
35386
35672
  onChange: (status) => broadcast({ type: "updates:changed", scope: "core", status })
35387
35673
  });
@@ -35549,7 +35835,7 @@ function hubBase() {
35549
35835
  }
35550
35836
  function serverChecker() {
35551
35837
  checker2 ??= createExtensionsChecker({
35552
- haloVersion: "2.2.0",
35838
+ haloVersion: "2.2.2",
35553
35839
  installed: () => {
35554
35840
  const s = getSnapshot();
35555
35841
  return [...s.extensions, ...s.disabled];
@@ -41920,7 +42206,7 @@ var init_dist2 = __esm({
41920
42206
  init_git_ssh();
41921
42207
  __dirname3 = path93.dirname(fileURLToPath3(import.meta.url));
41922
42208
  PORT = config.server.port;
41923
- HALO_VERSION = "2.2.0";
42209
+ HALO_VERSION = "2.2.2";
41924
42210
  PROJECT_ROOT = findProjectRoot();
41925
42211
  HALO_HOME4 = path93.join(homedir25(), ".halo");
41926
42212
  GIT_SHA = readGitSha();
@@ -42073,8 +42359,21 @@ var init_dist2 = __esm({
42073
42359
  }
42074
42360
  FRONTEND_DIR = resolveFrontendDir();
42075
42361
  if (!AGENTCORE) {
42076
- app.use("/*", serveStatic({ root: path93.relative(process.cwd(), FRONTEND_DIR) }));
42362
+ app.use("/*", serveStatic({
42363
+ root: path93.relative(process.cwd(), FRONTEND_DIR),
42364
+ // Next's `/_next/static/` files are content-hashed (under a buildId dir),
42365
+ // so they can be cached forever. Only on a found file — the SPA fallback
42366
+ // below must stay uncached, or a missing chunk caches index.html as js.
42367
+ // `c.res.headers`, not `c.header`: node-server calls onFound after the
42368
+ // body is built, so only c.res (merged into the result by compose) lands.
42369
+ onFound: (_path, c) => {
42370
+ if (c.req.path.startsWith("/_next/static/"))
42371
+ c.res.headers.set("Cache-Control", "public, max-age=31536000, immutable");
42372
+ }
42373
+ }));
42077
42374
  app.get("/*", (c) => {
42375
+ if (c.req.path.startsWith("/_next/static/"))
42376
+ return c.notFound();
42078
42377
  const indexPath = path93.join(FRONTEND_DIR, "index.html");
42079
42378
  try {
42080
42379
  const html = fs88.readFileSync(indexPath, "utf-8");
@@ -42102,6 +42401,7 @@ var init_dist2 = __esm({
42102
42401
  });
42103
42402
  wss = new WebSocketServer5({
42104
42403
  noServer: true,
42404
+ perMessageDeflate: { zlibDeflateOptions: { level: 1 }, threshold: 1024 },
42105
42405
  verifyClient: (info, callback) => {
42106
42406
  const token = getTokenFromCookieHeader(info.req.headers.cookie);
42107
42407
  if (isAuthenticated(token)) {
@@ -43981,7 +44281,7 @@ function runNode(opts) {
43981
44281
  shellTimeoutMs: config.timeout.shellExec,
43982
44282
  os: process.platform,
43983
44283
  hostname: os13.hostname(),
43984
- version: "2.2.0"
44284
+ version: "2.2.2"
43985
44285
  };
43986
44286
  const exec2 = createNodeExecutor({ root, base, allowShell }, log2);
43987
44287
  const instance = crypto17.randomUUID();
@@ -44497,7 +44797,7 @@ function installUserUnit(opts, run = runCommand) {
44497
44797
  }
44498
44798
 
44499
44799
  // src/index.ts
44500
- var VERSION = "2.2.0";
44800
+ var VERSION = "2.2.2";
44501
44801
  var HELP_TOP = `Usage: halo <command> [options]
44502
44802
 
44503
44803
  Commands: