privateboard 0.1.32 → 0.1.36

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/cli.js CHANGED
@@ -2640,7 +2640,13 @@ var MODELS = {
2640
2640
  baiId: "claude-opus-4.7",
2641
2641
  displayName: "Opus 4.7",
2642
2642
  contextBudget: 2e5,
2643
- deck: "deep reasoning"
2643
+ deck: "deep reasoning",
2644
+ // Anthropic dropped `temperature` for the 4.7 family · sending it
2645
+ // returns HTTP 400 "temperature is deprecated for this model"
2646
+ // across every carrier (direct / OR / B.AI all proxy to the same
2647
+ // upstream). The adapter omits temperature for any model with
2648
+ // this flag.
2649
+ noTemperature: true
2644
2650
  },
2645
2651
  "opus-4-6-fast": {
2646
2652
  v: "opus-4-6-fast",
@@ -2852,6 +2858,16 @@ function getModel(v) {
2852
2858
  function isModelV(v) {
2853
2859
  return Object.hasOwn(MODELS, v);
2854
2860
  }
2861
+ function noTemperatureModelIds() {
2862
+ const ids = /* @__PURE__ */ new Set();
2863
+ for (const m of Object.values(MODELS)) {
2864
+ if (!m.noTemperature) continue;
2865
+ if (m.directApiId) ids.add(m.directApiId);
2866
+ if (m.openrouterId) ids.add(m.openrouterId);
2867
+ if (m.baiId) ids.add(m.baiId);
2868
+ }
2869
+ return ids;
2870
+ }
2855
2871
 
2856
2872
  // src/routes/agents.ts
2857
2873
  init_persona_jobs();
@@ -4044,21 +4060,47 @@ function redactHeaderValue(name, value) {
4044
4060
  const tail = v.slice(-4);
4045
4061
  return tail ? `****${tail}` : "****";
4046
4062
  }
4063
+ var NO_TEMP_IDS_CACHE = null;
4064
+ function noTempIds() {
4065
+ if (!NO_TEMP_IDS_CACHE) NO_TEMP_IDS_CACHE = noTemperatureModelIds();
4066
+ return NO_TEMP_IDS_CACHE;
4067
+ }
4068
+ function stripTemperatureForNoTempModels(rawBody) {
4069
+ try {
4070
+ const parsed = JSON.parse(rawBody);
4071
+ const modelId = typeof parsed.model === "string" ? parsed.model : null;
4072
+ if (modelId && noTempIds().has(modelId) && "temperature" in parsed) {
4073
+ delete parsed.temperature;
4074
+ return { body: JSON.stringify(parsed), stripped: true };
4075
+ }
4076
+ } catch {
4077
+ }
4078
+ return { body: rawBody, stripped: false };
4079
+ }
4047
4080
  function makeLoggedFetch(tag) {
4048
4081
  return function loggedFetch2(input, init) {
4082
+ let effectiveInit = init;
4083
+ let stripNote = "";
4084
+ if (init?.body && typeof init.body === "string") {
4085
+ const r = stripTemperatureForNoTempModels(init.body);
4086
+ if (r.stripped) {
4087
+ effectiveInit = { ...init, body: r.body };
4088
+ stripNote = ` \xB7 stripped temperature (noTemperature model)`;
4089
+ }
4090
+ }
4049
4091
  const url = typeof input === "string" || input instanceof URL ? String(input) : input.url;
4050
- const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
4051
- const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
4092
+ const method = (effectiveInit?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
4093
+ const headers = new Headers(effectiveInit?.headers ?? (input instanceof Request ? input.headers : void 0));
4052
4094
  const headerLines = [];
4053
4095
  headers.forEach((v, k) => {
4054
4096
  headerLines.push(` ${k}: ${redactHeaderValue(k, v)}`);
4055
4097
  });
4056
4098
  let bodyPretty = "";
4057
- if (init?.body && typeof init.body === "string") {
4099
+ if (effectiveInit?.body && typeof effectiveInit.body === "string") {
4058
4100
  try {
4059
- bodyPretty = JSON.stringify(JSON.parse(init.body), null, 2);
4101
+ bodyPretty = JSON.stringify(JSON.parse(effectiveInit.body), null, 2);
4060
4102
  } catch {
4061
- bodyPretty = init.body.length > 2e3 ? init.body.slice(0, 2e3) + "\u2026" : init.body;
4103
+ bodyPretty = effectiveInit.body.length > 2e3 ? effectiveInit.body.slice(0, 2e3) + "\u2026" : effectiveInit.body;
4062
4104
  }
4063
4105
  }
4064
4106
  const sep = "\u2500".repeat(60);
@@ -4071,14 +4113,14 @@ function makeLoggedFetch(tag) {
4071
4113
  process.stderr.write(
4072
4114
  `
4073
4115
  \u250C${sep}
4074
- \u2502 [${tag} \u2192] ${method} ${url}
4116
+ \u2502 [${tag} \u2192] ${method} ${url}${stripNote}
4075
4117
  ` + headerBlock + `
4076
4118
  ` + bodyBlock + `
4077
4119
  \u2514${sep}
4078
4120
  `
4079
4121
  );
4080
4122
  const t0 = Date.now();
4081
- return fetch(input, init).then(async (res) => {
4123
+ return fetch(input, effectiveInit).then(async (res) => {
4082
4124
  const ms = Date.now() - t0;
4083
4125
  const resHeaderLines = [];
4084
4126
  res.headers.forEach((v, k) => resHeaderLines.push(` ${k}: ${v}`));
@@ -4339,6 +4381,7 @@ async function* callLLMStream(req) {
4339
4381
  yield { type: "error", message: formatStreamError(e) };
4340
4382
  return;
4341
4383
  }
4384
+ const temperature = getModel(req.modelV).noTemperature ? void 0 : req.temperature;
4342
4385
  let attempt = 0;
4343
4386
  let lastTransientMessage = "";
4344
4387
  let yieldedText = false;
@@ -4364,7 +4407,7 @@ async function* callLLMStream(req) {
4364
4407
  model: resolved.model,
4365
4408
  providerOptions: resolved.providerOptions,
4366
4409
  messages: req.messages,
4367
- temperature: req.temperature,
4410
+ temperature,
4368
4411
  // Vercel SDK names this maxOutputTokens in v4+; tolerate both.
4369
4412
  maxTokens: req.maxTokens,
4370
4413
  abortSignal: req.signal
@@ -7991,8 +8034,11 @@ var CLUSTER_MAX_SIZE = 60;
7991
8034
  var PROMOTE_MIN_PROVENANCE = 3;
7992
8035
  var PROMOTE_MIN_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
7993
8036
  var PROMOTE_MIN_CONFIDENCE = 0.6;
7994
- var USER_LONG_HARVEST_MIN_LONG = 5;
7995
- var USER_LONG_HARVEST_MIN_NEW_PROMOTED = 3;
8037
+ var USER_LONG_HARVEST_MIN_LONG = 2;
8038
+ var USER_LONG_HARVEST_MIN_NEW_PROMOTED = 1;
8039
+ var USER_LONG_HARVEST_MIN_SHORT_HIGH = 6;
8040
+ var USER_LONG_SHORT_CONF_FLOOR = 0.6;
8041
+ var USER_LONG_HARVEST_INPUT_CAP = 40;
7996
8042
  var USER_LONG_CAP = 30;
7997
8043
  async function runDreamCycle(agentId, config = {}) {
7998
8044
  const startedAt = Date.now();
@@ -8103,13 +8149,18 @@ async function runDreamCycle(agentId, config = {}) {
8103
8149
  if (!config.skipLLM && utility && agent?.roleKind === "moderator") {
8104
8150
  try {
8105
8151
  const chairLong = listTierForAgent(agentId, "long");
8106
- const eligible = chairLong.length >= USER_LONG_HARVEST_MIN_LONG || promoted >= USER_LONG_HARVEST_MIN_NEW_PROMOTED;
8152
+ const chairShortHigh = listTierForAgent(agentId, "short").filter((m) => m.confidence >= USER_LONG_SHORT_CONF_FLOOR && !m.pinned);
8153
+ const eligible = chairLong.length >= USER_LONG_HARVEST_MIN_LONG || promoted >= USER_LONG_HARVEST_MIN_NEW_PROMOTED || chairShortHigh.length >= USER_LONG_HARVEST_MIN_SHORT_HIGH;
8107
8154
  if (eligible) {
8108
8155
  const existing = listActiveUserLongMemory();
8156
+ const pool = [
8157
+ ...chairLong,
8158
+ ...chairShortHigh.slice().sort((a, b) => b.confidence - a.confidence)
8159
+ ].slice(0, USER_LONG_HARVEST_INPUT_CAP);
8109
8160
  const harvest = await harvestUserLongMemory({
8110
8161
  modelV: utility,
8111
8162
  userName,
8112
- chairLong,
8163
+ chairLong: pool,
8113
8164
  existing
8114
8165
  });
8115
8166
  for (const t of harvest.newTags) {
@@ -8198,14 +8249,14 @@ async function runDreamCycle(agentId, config = {}) {
8198
8249
  var HARVEST_EMPTY = { newTags: [], reinforce: [], supersede: [] };
8199
8250
  function buildHarvestPrompt(opts) {
8200
8251
  const existingBlock = opts.existing.length === 0 ? "(no existing tags yet)" : opts.existing.map((t) => `[${t.id}] ${t.label} \xB7 ${t.claim} \xB7 provenance=${t.provenanceRooms}`).join("\n");
8201
- const chairBlock = opts.chairLong.length === 0 ? "(no long-tier chair memories yet)" : opts.chairLong.map((m) => `\xB7 (${m.kind}, conf=${m.confidence.toFixed(2)}, rooms=${m.provenanceRooms}) ${m.content}`).join("\n");
8252
+ const chairBlock = opts.chairLong.length === 0 ? "(no chair memories yet)" : opts.chairLong.map((m) => `\xB7 (${m.kind}, conf=${m.confidence.toFixed(2)}, rooms=${m.provenanceRooms}, tier=${m.tier}) ${m.content}`).join("\n");
8202
8253
  return [
8203
- `You are reviewing the chair's long-term memories about ${opts.userName} to extract durable, tag-shaped abstractions that should live in a separate sanctuary table (never decayed, only displaced on direct contradiction).`,
8254
+ `You are reviewing the chair's high-conviction memories about ${opts.userName} to extract durable, tag-shaped abstractions that should live in a separate sanctuary table (never decayed, only displaced on direct contradiction).`,
8204
8255
  ``,
8205
8256
  `## Existing user-long-memory tags`,
8206
8257
  existingBlock,
8207
8258
  ``,
8208
- `## Chair's long-tier memories about ${opts.userName}`,
8259
+ `## Chair memories about ${opts.userName} (mixed pool \xB7 prefer long-tier or high-confidence short-tier entries when proposing tags)`,
8209
8260
  chairBlock,
8210
8261
  ``,
8211
8262
  `## Output`,
@@ -14968,161 +15019,117 @@ function listConfiguredVoices() {
14968
15019
  });
14969
15020
  return out;
14970
15021
  }
14971
- async function listAvailableVoices() {
14972
- const activeProvider = getActiveVoiceProvider();
14973
- let voices = listConfiguredVoices();
14974
- if (!activeProvider) {
14975
- return { voices, provider: null, configured: false };
15022
+ function encodeCursor(c) {
15023
+ return Buffer.from(JSON.stringify(c), "utf8").toString("base64url");
15024
+ }
15025
+ function decodeCursor(s) {
15026
+ if (!s) return null;
15027
+ try {
15028
+ const obj = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
15029
+ if (obj && (obj.src === "el" || obj.src === "mm")) return obj;
15030
+ } catch {
14976
15031
  }
14977
- const activeKey = getActiveVoiceKeyPlaintext();
14978
- if (!activeKey) {
14979
- return { voices, provider: activeProvider, configured: false };
15032
+ return null;
15033
+ }
15034
+ function classifyElevenLabsError(status, body) {
15035
+ let parsed = null;
15036
+ try {
15037
+ parsed = JSON.parse(body);
15038
+ } catch {
14980
15039
  }
14981
- if (activeProvider === "minimax") {
15040
+ const detail = parsed?.detail;
15041
+ const upstreamStatus = typeof detail?.status === "string" ? detail.status : "";
15042
+ const upstreamMessage = typeof detail?.message === "string" ? detail.message : body.slice(0, 200);
15043
+ if (status === 401 && upstreamStatus === "missing_permissions") {
15044
+ return {
15045
+ code: "missing_permissions",
15046
+ provider: "elevenlabs",
15047
+ message: upstreamMessage,
15048
+ // Direct link to the API-key management page · "Update key
15049
+ // permissions" is what the user needs to do, and ElevenLabs's
15050
+ // settings page surfaces the scope checkboxes prominently.
15051
+ fixUrl: "https://elevenlabs.io/app/settings/api-keys"
15052
+ };
15053
+ }
15054
+ if (status === 401 || status === 403) {
15055
+ return {
15056
+ code: "auth_failed",
15057
+ provider: "elevenlabs",
15058
+ message: upstreamMessage,
15059
+ fixUrl: "https://elevenlabs.io/app/settings/api-keys"
15060
+ };
15061
+ }
15062
+ if (status === 429) {
15063
+ return {
15064
+ code: "rate_limited",
15065
+ provider: "elevenlabs",
15066
+ message: upstreamMessage
15067
+ };
15068
+ }
15069
+ return {
15070
+ code: "fetch_failed",
15071
+ provider: "elevenlabs",
15072
+ message: `HTTP ${status}: ${upstreamMessage}`
15073
+ };
15074
+ }
15075
+ async function fetchAllElevenLabsV2Voices(apiKey) {
15076
+ const out = [];
15077
+ let token = null;
15078
+ let lastError = null;
15079
+ for (let i = 0; i < 20; i++) {
15080
+ const url = new URL("https://api.elevenlabs.io/v2/voices");
15081
+ url.searchParams.set("page_size", "100");
15082
+ if (token) url.searchParams.set("next_page_token", token);
14982
15083
  try {
14983
- const res = await fetch(`${minimaxBaseUrl()}/v1/get_voice`, {
14984
- method: "POST",
14985
- headers: {
14986
- "authorization": `Bearer ${activeKey}`,
14987
- "content-type": "application/json"
14988
- },
14989
- body: JSON.stringify({ voice_type: "all" })
15084
+ const res = await fetch(url.toString(), {
15085
+ headers: { "xi-api-key": apiKey }
14990
15086
  });
14991
- if (res.ok) {
14992
- const json = await res.json();
14993
- const rows = [
14994
- ...voiceRows(json.system_voice, "system"),
14995
- ...voiceRows(json.voice_cloning, "clone"),
14996
- ...voiceRows(json.voice_generation, "generated")
14997
- ];
14998
- if (rows.length > 0) {
14999
- const nonMiniMax = voices.filter((v) => v.provider !== "minimax");
15000
- voices = [
15001
- ...nonMiniMax,
15002
- ...rows.map((r) => ({
15003
- provider: "minimax",
15004
- model: "speech-2.8-hd",
15005
- voiceId: r.voiceId,
15006
- label: r.label,
15007
- language: r.kind,
15008
- configured: true
15009
- }))
15010
- ];
15011
- }
15012
- }
15013
- } catch {
15014
- }
15015
- return { voices, provider: "minimax", configured: true };
15016
- }
15017
- if (activeProvider === "elevenlabs") {
15018
- const personal = [];
15019
- const shared = [];
15020
- await Promise.all([
15021
- (async () => {
15022
- try {
15023
- const res = await fetch(
15024
- "https://api.elevenlabs.io/v1/voices?show_legacy=true&include_total_count=true",
15025
- { headers: { "xi-api-key": activeKey } }
15026
- );
15027
- if (!res.ok) {
15028
- const errText = await res.text();
15029
- process.stderr.write(
15030
- `[voice-registry] elevenlabs /v1/voices HTTP ${res.status}: ${errText.slice(0, 300)}
15031
- `
15032
- );
15033
- return;
15034
- }
15035
- const json = await res.json();
15036
- const rows = elevenLabsVoiceRows(json.voices);
15037
- process.stderr.write(`[voice-registry] elevenlabs /v1/voices \xB7 ${rows.length} voices in personal library
15038
- `);
15039
- personal.push(...rows);
15040
- } catch (e) {
15041
- const cause = e instanceof Error ? e.cause : null;
15042
- const detail = cause?.message ? `: ${cause.message}` : "";
15043
- process.stderr.write(
15044
- `[voice-registry] elevenlabs /v1/voices fetch failed${detail} \xB7 ${e instanceof Error ? e.message : String(e)}
15045
- `
15046
- );
15047
- }
15048
- })(),
15049
- (async () => {
15050
- try {
15051
- const res = await fetch(
15052
- "https://api.elevenlabs.io/v1/shared-voices?page_size=100",
15053
- { headers: { "xi-api-key": activeKey } }
15054
- );
15055
- if (!res.ok) {
15056
- const errText = await res.text();
15057
- process.stderr.write(
15058
- `[voice-registry] elevenlabs /v1/shared-voices HTTP ${res.status}: ${errText.slice(0, 300)}
15087
+ if (!res.ok) {
15088
+ const errText = await res.text();
15089
+ process.stderr.write(
15090
+ `[voice-registry] elevenlabs /v2/voices HTTP ${res.status}: ${errText.slice(0, 300)}
15059
15091
  `
15060
- );
15061
- return;
15062
- }
15063
- const json = await res.json();
15064
- const rows = elevenLabsSharedVoiceRows(json.voices);
15065
- process.stderr.write(`[voice-registry] elevenlabs /v1/shared-voices \xB7 ${rows.length} voices from public library
15066
- `);
15067
- shared.push(...rows);
15068
- } catch (e) {
15069
- const cause = e instanceof Error ? e.cause : null;
15070
- const detail = cause?.message ? `: ${cause.message}` : "";
15071
- process.stderr.write(
15072
- `[voice-registry] elevenlabs /v1/shared-voices fetch failed${detail} \xB7 ${e instanceof Error ? e.message : String(e)}
15092
+ );
15093
+ lastError = classifyElevenLabsError(res.status, errText);
15094
+ break;
15095
+ }
15096
+ const json = await res.json();
15097
+ const rows = elevenLabsV2VoiceRows(json.voices);
15098
+ for (const r of rows) {
15099
+ out.push({
15100
+ provider: "elevenlabs",
15101
+ model: "eleven_multilingual_v2",
15102
+ voiceId: r.voiceId,
15103
+ label: r.label,
15104
+ language: r.category,
15105
+ configured: true
15106
+ });
15107
+ }
15108
+ const nextToken = json.has_more === true && typeof json.next_page_token === "string" ? json.next_page_token : null;
15109
+ if (!nextToken) break;
15110
+ token = nextToken;
15111
+ } catch (e) {
15112
+ const cause = e instanceof Error ? e.cause : null;
15113
+ const detail = cause?.message ? `: ${cause.message}` : "";
15114
+ process.stderr.write(
15115
+ `[voice-registry] elevenlabs /v2/voices fetch failed${detail} \xB7 ${e instanceof Error ? e.message : String(e)}
15073
15116
  `
15074
- );
15075
- }
15076
- })()
15077
- ]);
15078
- if (personal.length > 0 || shared.length > 0) {
15079
- const nonEl = voices.filter((v) => v.provider !== "elevenlabs");
15080
- const personalIds = new Set(personal.map((r) => r.voiceId));
15081
- const sharedDeduped = shared.filter((r) => !personalIds.has(r.voiceId));
15082
- const personalMapped = personal.map((r) => ({
15083
- provider: "elevenlabs",
15084
- model: "eleven_multilingual_v2",
15085
- voiceId: r.voiceId,
15086
- label: r.label,
15087
- // Personal-library rows keep their actual category
15088
- // ("premade", "cloned", "professional", "generated").
15089
- language: r.category,
15090
- configured: true
15091
- }));
15092
- const sharedMapped = sharedDeduped.map((r) => ({
15117
+ );
15118
+ lastError = {
15119
+ code: "fetch_failed",
15093
15120
  provider: "elevenlabs",
15094
- model: "eleven_multilingual_v2",
15095
- voiceId: r.voiceId,
15096
- // Prefix shared-library voices so users can tell at a glance
15097
- // which set they're picking from. The dropdown's group header
15098
- // already says "elevenlabs", so the per-row prefix is the
15099
- // tightest signal we have for personal-vs-shared.
15100
- label: `${r.label} \xB7 shared`,
15101
- language: r.language || r.category,
15102
- configured: true
15103
- }));
15104
- voices = [...nonEl, ...personalMapped, ...sharedMapped];
15121
+ message: e instanceof Error ? e.message : String(e)
15122
+ };
15123
+ break;
15105
15124
  }
15106
- return { voices, provider: "elevenlabs", configured: true };
15107
15125
  }
15108
- return { voices, provider: activeProvider, configured: true };
15109
- }
15110
- function elevenLabsSharedVoiceRows(raw) {
15111
- if (!Array.isArray(raw)) return [];
15112
- const out = [];
15113
- for (const item of raw) {
15114
- if (!item || typeof item !== "object") continue;
15115
- const obj = item;
15116
- const voiceId = typeof obj.voice_id === "string" ? obj.voice_id : "";
15117
- if (!voiceId) continue;
15118
- const label = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : voiceId;
15119
- const category = typeof obj.category === "string" && obj.category.trim() ? obj.category.trim() : "shared";
15120
- const language = typeof obj.language === "string" && obj.language.trim() ? obj.language.trim() : void 0;
15121
- out.push({ voiceId, label, category, language });
15122
- }
15123
- return out;
15126
+ process.stderr.write(
15127
+ `[voice-registry] elevenlabs /v2/voices \xB7 ${out.length} voices total across all pages
15128
+ `
15129
+ );
15130
+ return { voices: out, error: lastError };
15124
15131
  }
15125
- function elevenLabsVoiceRows(raw) {
15132
+ function elevenLabsV2VoiceRows(raw) {
15126
15133
  if (!Array.isArray(raw)) return [];
15127
15134
  const out = [];
15128
15135
  for (const item of raw) {
@@ -15136,6 +15143,164 @@ function elevenLabsVoiceRows(raw) {
15136
15143
  }
15137
15144
  return out;
15138
15145
  }
15146
+ var ELEVENLABS_CACHE_TTL_MS = 5 * 60 * 1e3;
15147
+ var elevenLabsCache = /* @__PURE__ */ new Map();
15148
+ function elevenLabsCacheKey(apiKey) {
15149
+ return apiKey.slice(0, 8);
15150
+ }
15151
+ async function getElevenLabsVoicesCached(apiKey) {
15152
+ const key = elevenLabsCacheKey(apiKey);
15153
+ const cached = elevenLabsCache.get(key);
15154
+ if (cached && cached.expiresAt > Date.now()) {
15155
+ return { voices: cached.voices, error: null };
15156
+ }
15157
+ const result = await fetchAllElevenLabsV2Voices(apiKey);
15158
+ process.stderr.write(
15159
+ `[voice-registry] elevenlabs catalogue \xB7 ${result.voices.length} voices from /v2/voices${result.error ? ` (error: ${result.error.code})` : ""}
15160
+ `
15161
+ );
15162
+ if (!result.error) {
15163
+ elevenLabsCache.set(key, { voices: result.voices, expiresAt: Date.now() + ELEVENLABS_CACHE_TTL_MS });
15164
+ }
15165
+ return result;
15166
+ }
15167
+ var MINIMAX_CACHE_TTL_MS = 5 * 60 * 1e3;
15168
+ var miniMaxCache = /* @__PURE__ */ new Map();
15169
+ function miniMaxCacheKey(apiKey) {
15170
+ return apiKey.slice(0, 8);
15171
+ }
15172
+ async function fetchAllMiniMaxVoices(apiKey) {
15173
+ try {
15174
+ const res = await fetch(`${minimaxBaseUrl()}/v1/get_voice`, {
15175
+ method: "POST",
15176
+ headers: {
15177
+ "authorization": `Bearer ${apiKey}`,
15178
+ "content-type": "application/json"
15179
+ },
15180
+ body: JSON.stringify({ voice_type: "all" })
15181
+ });
15182
+ if (!res.ok) {
15183
+ return MINIMAX_SYSTEM_VOICES.map((v) => ({ ...v, configured: true }));
15184
+ }
15185
+ const json = await res.json();
15186
+ const rows = [
15187
+ ...voiceRows(json.system_voice, "system"),
15188
+ ...voiceRows(json.voice_cloning, "clone"),
15189
+ ...voiceRows(json.voice_generation, "generated")
15190
+ ];
15191
+ if (rows.length === 0) {
15192
+ return MINIMAX_SYSTEM_VOICES.map((v) => ({ ...v, configured: true }));
15193
+ }
15194
+ return rows.map((r) => ({
15195
+ provider: "minimax",
15196
+ model: "speech-2.8-hd",
15197
+ voiceId: r.voiceId,
15198
+ label: r.label,
15199
+ language: r.kind,
15200
+ configured: true
15201
+ }));
15202
+ } catch {
15203
+ return MINIMAX_SYSTEM_VOICES.map((v) => ({ ...v, configured: true }));
15204
+ }
15205
+ }
15206
+ async function getMiniMaxVoicesCached(apiKey) {
15207
+ const key = miniMaxCacheKey(apiKey);
15208
+ const cached = miniMaxCache.get(key);
15209
+ if (cached && cached.expiresAt > Date.now()) return cached.voices;
15210
+ const voices = await fetchAllMiniMaxVoices(apiKey);
15211
+ miniMaxCache.set(key, { voices, expiresAt: Date.now() + MINIMAX_CACHE_TTL_MS });
15212
+ return voices;
15213
+ }
15214
+ var BROWSER_FALLBACK = {
15215
+ provider: "browser",
15216
+ model: "speechSynthesis",
15217
+ voiceId: "system-default",
15218
+ label: "Browser default",
15219
+ configured: true
15220
+ };
15221
+ async function listVoicesPage(cursorStr, pageSize) {
15222
+ const size = Math.min(Math.max(pageSize | 0 || 30, 5), 100);
15223
+ const cursor = decodeCursor(cursorStr);
15224
+ const isFirstPage = cursor === null;
15225
+ const activeProvider = getActiveVoiceProvider();
15226
+ const fixed = [];
15227
+ if (isFirstPage && getKey("openai")) {
15228
+ fixed.push(...OPENAI_VOICES.map((v) => ({ ...v, configured: true })));
15229
+ }
15230
+ if (!activeProvider) {
15231
+ return {
15232
+ voices: [...fixed, BROWSER_FALLBACK],
15233
+ nextCursor: null,
15234
+ hasMore: false,
15235
+ provider: null,
15236
+ configured: false
15237
+ };
15238
+ }
15239
+ const activeKey = getActiveVoiceKeyPlaintext();
15240
+ if (!activeKey) {
15241
+ return {
15242
+ voices: [...fixed, BROWSER_FALLBACK],
15243
+ nextCursor: null,
15244
+ hasMore: false,
15245
+ provider: activeProvider,
15246
+ configured: false
15247
+ };
15248
+ }
15249
+ if (activeProvider === "elevenlabs") {
15250
+ const { voices: all, error } = await getElevenLabsVoicesCached(activeKey);
15251
+ const offset = cursor && cursor.src === "el" ? cursor.offset ?? 0 : 0;
15252
+ const slice = all.slice(offset, offset + size);
15253
+ const next = offset + slice.length;
15254
+ const hasMore = next < all.length;
15255
+ const nextCursor = hasMore ? encodeCursor({ src: "el", offset: next }) : null;
15256
+ const voices = [...fixed, ...slice];
15257
+ if (!hasMore) voices.push(BROWSER_FALLBACK);
15258
+ return {
15259
+ voices,
15260
+ nextCursor,
15261
+ hasMore,
15262
+ provider: "elevenlabs",
15263
+ configured: true,
15264
+ // Only attach the error to the FIRST page response · subsequent
15265
+ // pages (offset > 0) won't fire if the first page errored
15266
+ // (voices is empty so hasMore is false), but defensive.
15267
+ ...error && offset === 0 ? { error } : {}
15268
+ };
15269
+ }
15270
+ if (activeProvider === "minimax") {
15271
+ const all = await getMiniMaxVoicesCached(activeKey);
15272
+ const offset = cursor && cursor.src === "mm" ? cursor.offset ?? 0 : 0;
15273
+ const slice = all.slice(offset, offset + size);
15274
+ const next = offset + slice.length;
15275
+ const hasMore = next < all.length;
15276
+ const nextCursor = hasMore ? encodeCursor({ src: "mm", offset: next }) : null;
15277
+ const voices = [...fixed, ...slice];
15278
+ if (!hasMore) voices.push(BROWSER_FALLBACK);
15279
+ return { voices, nextCursor, hasMore, provider: "minimax", configured: true };
15280
+ }
15281
+ return {
15282
+ voices: [...fixed, BROWSER_FALLBACK],
15283
+ nextCursor: null,
15284
+ hasMore: false,
15285
+ provider: activeProvider,
15286
+ configured: true
15287
+ };
15288
+ }
15289
+ async function listAvailableVoices() {
15290
+ const voices = [];
15291
+ let cursor = null;
15292
+ let provider = null;
15293
+ let configured = false;
15294
+ for (let i = 0; i < 50; i++) {
15295
+ const page = await listVoicesPage(cursor, 100);
15296
+ voices.push(...page.voices);
15297
+ provider = page.provider;
15298
+ configured = page.configured;
15299
+ if (!page.hasMore || !page.nextCursor) break;
15300
+ cursor = page.nextCursor;
15301
+ }
15302
+ return { voices, provider, configured };
15303
+ }
15139
15304
  function voiceRows(raw, kind) {
15140
15305
  if (!Array.isArray(raw)) return [];
15141
15306
  const out = [];
@@ -15170,7 +15335,7 @@ function makeMiniMaxBalanceError() {
15170
15335
  );
15171
15336
  err2.code = "paid-plan-required";
15172
15337
  err2.provider = "minimax";
15173
- err2.upgradeUrl = getPrefs().minimaxRegion === "intl" ? "https://platform.minimax.io/user-center/billing/overview" : "https://platform.minimaxi.com/user-center/payment";
15338
+ err2.upgradeUrl = getPrefs().minimaxRegion === "intl" ? "https://platform.minimax.io/user-center/basic-information" : "https://platform.minimaxi.com/user-center/payment/balance";
15174
15339
  return err2;
15175
15340
  }
15176
15341
  function makeElevenLabsBillingError(message) {
@@ -15197,10 +15362,15 @@ function tryExtractTtsBillingError(err2) {
15197
15362
  }
15198
15363
  return out;
15199
15364
  }
15365
+ function stripSpokenLabels(text) {
15366
+ if (!text) return "";
15367
+ return text.replace(/【[^】\n]{1,40}】[ \t]*/g, "").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
15368
+ }
15200
15369
  function cleanForSpeech(md) {
15201
15370
  if (!md) return "";
15202
15371
  let out = md;
15203
15372
  out = out.replace(/```[\s\S]*?```/g, " ");
15373
+ out = out.replace(/【[^】\n]{1,40}】[ \t]*/g, " ");
15204
15374
  out = out.replace(/`([^`\n]+)`/g, "$1");
15205
15375
  out = out.replace(/!\[[^\]]*\]\([^)]+\)/g, " ");
15206
15376
  out = out.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
@@ -17996,6 +18166,7 @@ var MAX_PICKS = 2;
17996
18166
  async function pickChairClarifyDecision(opts) {
17997
18167
  const prompt = latestUserPrompt(opts.history);
17998
18168
  if (!prompt) return { shouldAsk: true, rationale: "no user prompt yet" };
18169
+ const isBrainstorm = (opts.mode || "").toLowerCase() === "brainstorm";
17999
18170
  const sys = {
18000
18171
  role: "system",
18001
18172
  content: [
@@ -18017,6 +18188,14 @@ async function pickChairClarifyDecision(opts) {
18017
18188
  "Bias toward RELEASE. A slightly-fuzzy framing is fine \u2014 directors",
18018
18189
  "can sharpen it themselves. Asking when you don't need to kills",
18019
18190
  "momentum.",
18191
+ ...isBrainstorm ? [
18192
+ "",
18193
+ "BRAINSTORM MODE OVERRIDE \xB7 this room is in brainstorm mode. RELEASE",
18194
+ "unless the subject is literally unparseable (empty, gibberish, single",
18195
+ "character). Fuzzy / abstract / under-specified seeds are a FEATURE",
18196
+ "here \u2014 directors fill the gap with explicit assumptions, not by",
18197
+ "asking the user. Default ask=false in brainstorm."
18198
+ ] : [],
18020
18199
  "",
18021
18200
  "Reply with STRICT JSON ONLY (no prose, no fences):",
18022
18201
  `{ "ask": true, "rationale": "\u2264120 chars \xB7 what's load-bearingly missing" }`,
@@ -18843,47 +19022,49 @@ var SHARED_ROOM_PROTOCOL = [
18843
19022
  ].join("\n");
18844
19023
  var TONE_GUIDANCE = {
18845
19024
  brainstorm: [
18846
- `BRAINSTORM \xB7 the room's job is to EXPAND THE POSSIBILITY SPACE \u2014 fast, plenty, without rabbit holes. Real-life brainstorm: people throw ideas ("we could do this, we could do that"), pile them up, filter later. **Volume over polish. Quantity over rigor. Yes-and over yes-but.**`,
19025
+ "\u2500\u2500\u2500 \u5171\u521B\u6A21\u5F0F \xB7 BRAINSTORM \u2500\u2500\u2500",
19026
+ "\u4F60\u4EEC\u662F\u7528\u6237\u7684\u3010\u591A\u89D2\u8272\u5171\u521B\u56E2\u961F\u3011\uFF0C\u4E0D\u662F\u3010\u8BC4\u5BA1\u56E2\u3011\u3002\u4EFB\u52A1\u662F\u5E2E\u7528\u6237\u53D1\u73B0 idea \u7684\u4EF7\u503C\u3001\u653E\u5927\u5B83\u3001\u5EF6\u5C55\u5B83\u3001\u63D0\u51FA\u66F4\u6709\u542F\u53D1\u7684\u65B0\u65B9\u5411\uFF0C\u5E76\u5E2E ta \u628A idea \u53D8\u6210\u66F4\u6709\u60F3\u8C61\u529B\u3001\u66F4\u53EF\u4F20\u64AD\u3001\u66F4\u53EF\u843D\u5730\u7684\u65B9\u6848\u3002",
18847
19027
  "",
18848
- "## How a turn looks",
18849
- "Each turn produces **3\u20136 ideas, NOT one**. Each idea is **1\u20132 sentences, NOT a thesis**. Don't write four labelled paragraphs per idea \u2014 just say the idea.",
19028
+ "\u9ED8\u8BA4\u6A21\u5F0F\uFF1A**\u53D1\u6563\u5171\u521B\u6A21\u5F0F\uFF08VALUE AMPLIFICATION\uFF09**\u3002",
18850
19029
  "",
18851
- "Format \xB7 a quick bulleted list:",
18852
- " \xB7 <idea 1 \xB7 1\u20132 sentences \xB7 concrete>",
18853
- " \xB7 <idea 2 \xB7 1\u20132 sentences \xB7 different angle, OR yes-and on a previous one>",
18854
- " \xB7 <idea 3 \xB7 1\u20132 sentences \xB7 go wilder>",
18855
- " \xB7 ... (3\u20136 total)",
19030
+ "## \u7EDD\u5BF9\u4E0D\u8981 (do NOT)",
19031
+ " \xB7 \u4E0D\u8981\u6025\u7740\u5224\u65AD\u5BF9\u9519\uFF1B",
19032
+ " \xB7 \u4E0D\u8981\u9891\u7E41\u5411\u7528\u6237\u63D0\u95EE\uFF08\u6574\u8F6E\u91CC\u6700\u591A 1 \u4E2A\u771F\u6B63\u5FC5\u8981\u7684\u95EE\u9898\uFF0C\u4E14\u5FC5\u987B\u5148\u7ED9\u51FA\u81EA\u5DF1\u7684\u5224\u65AD\u548C\u5EFA\u8BAE\uFF0C\u95EE\u53E5\u4E0D\u80FD\u66FF\u4EE3\u5224\u65AD\uFF09\uFF1B",
19033
+ " \xB7 \u4E0D\u8981\u4EE5\u300C\u627E\u6F0F\u6D1E / \u98CE\u9669 / \u4E0D\u53EF\u884C\u6027 / \u8FB9\u754C\u6761\u4EF6\u300D\u4E3A\u53D1\u8A00\u4E3B\u7EBF\uFF1B",
19034
+ " \xB7 \u4E0D\u8981\u628A\u8BA8\u8BBA\u6536\u655B\u5230\u98CE\u9669\u548C\u9650\u5236\uFF1B",
19035
+ ' \xB7 \u4FE1\u606F\u4E0D\u8DB3\u65F6\uFF0C\u8BF7**\u81EA\u884C\u505A\u5408\u7406\u5047\u8BBE\u5E76\u660E\u786E\u5199\u51FA**\uFF08"\u5047\u8BBE\u7528\u6237\u6307\u7684\u662F X\uFF0C\u90A3\u4E48\u2026"\uFF09\uFF1B\u4E0D\u8981\u56E0\u4E3A\u7F3A\u4FE1\u606F\u5C31\u505C\u4E0B\u6765\u53CD\u95EE\uFF1B',
19036
+ ' \xB7 \u6BCF\u6B21\u53D1\u8A00\u90FD\u5FC5\u987B\u8D21\u732E**\u65B0\u60F3\u6CD5**\u2014\u2014\u7EAF\u8BC4\u4EF7\uFF08"\u597D\u60F3\u6CD5\uFF0C\u4F46\u662F\u2026" / "\u4F60\u7684\u65B9\u5411\u662F\u5BF9\u7684\uFF0C\u9700\u8981\u6CE8\u610F\u2026"\uFF09\u4E0D\u7B97\u8D21\u732E\u3002',
18856
19037
  "",
18857
- "If a single idea genuinely needs one extra clause to be legible, add it inline. Don't structure it as `Angle: / Idea: / Why: / Opens up:` \u2014 that turns brainstorm into thesis-defense.",
19038
+ "## \u5F3A\u5236\u8F93\u51FA\u683C\u5F0F\uFF08\u6BCF\u4E2A director \u5FC5\u987B\u6309\u6B64\u586B\u5199\uFF0C\u4E0D\u5F97\u8DF3\u8FC7\u3001\u4E0D\u5F97\u6539\u540D\u3001\u4E0D\u5F97\u5408\u5E76\uFF09",
18858
19039
  "",
18859
- "## Three legitimate moves (mix freely in one turn)",
18860
- " \xB7 **NEW** \xB7 open a direction the room hasn't touched. Pick a lens nobody else has used (user pain \xB7 product form \xB7 workflow \xB7 market \xB7 business model \xB7 distribution \xB7 tech \xB7 behaviour \xB7 org \xB7 cross-industry analogy \xB7 future scenario \xB7 contrarian \xB7 hidden constraint \xB7 emotional motivation).",
18861
- ' \xB7 **YES-AND** \xB7 take a previous idea and extend / variant / combine it. Three flavours of one idea = three ideas. Combinations of two prior ideas = first-class contribution. "What Socrates said + a layer where X" is exactly what real brainstorm does \u2014 refer to peers by NAME, never by `@handle`.',
18862
- ` \xB7 **WILD** \xB7 the half-baked, the fanciful, the "what if we just". 20-30% of brainstorm value is the unexpected anchor a wild idea drops \u2014 even if the wild idea itself doesn't ship, it shifts what the next person can imagine.`,
19040
+ "\u3010\u6211\u770B\u5230\u7684\u4EF7\u503C\u3011",
19041
+ "1\u20133 \u53E5\u3002\u4ECE\u4F60\u7684\u4E13\u4E1A\u89C6\u89D2\u8BF4\u51FA\u8FD9\u4E2A idea \u91CC\u4F60\u55C5\u5230\u7684**\u771F\u6B63\u4EF7\u503C**\u3002\u5148\u653E\u5927\u5B83\uFF0C\u522B\u5148\u8D28\u7591\u5B83\u3002\u89E3\u91CA\u4E3A\u4EC0\u4E48\u8FD9\u4E2A\u4EF7\u503C\u662F\u771F\u7684\u3001\u4E3A\u4EC0\u4E48\u503C\u5F97\u88AB\u770B\u89C1\u3002",
18863
19042
  "",
18864
- "Aim for a mix across a turn: one or two NEW, one or two YES-AND, at least one WILD when you're stretching.",
19043
+ "\u3010\u6211\u4F1A\u600E\u4E48\u653E\u5927\u3011",
19044
+ "1\u20133 \u53E5\u3002\u5982\u679C\u8BA9\u4F60\u628A\u8FD9\u4E2A idea **\u7FFB\u500D / \u63A8\u5230\u66F4\u5927\u7684\u5C3A\u5EA6 / \u62D3\u5C55\u5230\u76F8\u90BB\u573A\u666F**\uFF0C\u4F60\u4F1A\u600E\u4E48\u505A\uFF1F\u7ED9\u4E00\u4E2A\u5177\u4F53\u7684\u653E\u5927\u65B9\u5411\u3002",
18865
19045
  "",
18866
- "## Don't",
18867
- ` \xB7 Don't pre-filter. "This probably won't work because\u2026" is forbidden. Feasibility judgement happens in critique mode, not here.`,
18868
- " \xB7 Don't go deep on one idea when five more are waiting. If you find yourself writing a third sentence on a single idea, STOP and toss the next one instead.",
18869
- ` \xB7 Don't preface with affirmation ("That's a good point, what if we\u2026") \u2014 just say the new idea.`,
18870
- " \xB7 Don't defend an idea once it's raised; let it stand or fall on its own. The room's job is generation, not protectiveness.",
18871
- ` \xB7 **Don't stamp each idea with a literal NEW: / YES-AND: / WILD: heading** (or their Chinese counterparts: "\u65B0\uFF1A" / "\u5EF6\u4F38\uFF1A" / "\u72C2\u91CE\uFF1A"). Those three are *descriptions of moves you can do across a turn*, not labels you stick on every bullet. Just say the idea \u2014 the reader can see whether it's a fresh direction, a yes-and on something prior, or a wild stretch from the content itself.`,
18872
- ` \xB7 Avoid generic innovation language: "synergy", "leverage AI", "platform play", "democratise X", "AI-native", "unlock value". They're decoration, not ideas.`,
18873
- " \xB7 Avoid the same lens used by the immediately-prior speaker UNLESS you're explicitly yes-and'ing them.",
19046
+ "\u3010\u4E00\u4E2A\u66F4\u6027\u611F\u7684\u8868\u8FBE\u3011",
19047
+ "1\u20132 \u53E5\u3002\u7ED9\u8FD9\u4E2A idea \u4E00\u4E2A**\u66F4\u6709\u4F20\u64AD\u529B\u7684\u8BF4\u6CD5**\u2014\u2014\u4E00\u53E5 slogan\u3001\u4E00\u4E2A\u65B0\u540D\u5B57\u3001\u4E00\u4E2A\u5BF9\u5916\u8BB2\u5F97\u6E05\u695A\u7684\u5B9A\u4F4D\u3001\u4E00\u4E2A\u8BA9\u4EBA\u8BB0\u4F4F\u7684\u6BD4\u55BB\u3002",
18874
19048
  "",
18875
- "## Optional \xB7 one synthesis turn per round",
18876
- "Once per round, ONE director may do a synthesis turn instead of pure generation: pick 2\u20133 of the room's ideas and propose 1\u20132 combinations. Use sparingly \u2014 80%+ of turns should be pure generation. The room's value is in the pile of ideas, not the polish on any one.",
19049
+ "\u3010\u4E00\u4E2A\u5177\u4F53\u505A\u6CD5\u3011",
19050
+ "1\u20133 \u53E5\u3002\u7ED9\u4E00\u4E2A**\u6700\u5C0F\u53EF\u6267\u884C**\u7684\u5177\u4F53\u52A8\u4F5C / \u5B9E\u9A8C / \u7B2C\u4E00\u6B65\u5F62\u6001\u3002**\u4E0B\u5468\u5C31\u80FD\u505A\u7684\u4E8B**\uFF0C\u4E0D\u662F\u5B8F\u5927\u84DD\u56FE\u3002",
18877
19051
  "",
18878
- "PERSONA OVERRIDE \xB7 your director instruction reads like Socrates / First Principles / Value Investor \u2014 the demand-definition / demand-mechanism / demand-base-rate DNA. For THIS room, the **rigor / forensic mode** of that DNA is paused (no slow definition-check ladders, no methodical mechanism decomposition, no insistence on base-rate citation before moving) \u2014 that's too slow for brainstorm cadence. But the **contrarian / curiosity / lens-distinctness** half of your DNA is *AMPLIFIED*, not paused. Your persona's signature angles (your loadBearingConcepts, your contrarianTakes, your failureModes) are exactly what produces the unexpected ideas the room needs.",
19052
+ "\u3010\u6211\u8865\u5145\u7684\u65B0\u65B9\u5411\u3011",
19053
+ "1\u20133 \u53E5\u3002\u4ECE\u4F60\u72EC\u7279\u7684\u89D2\u8272\u89C6\u89D2\uFF0C\u5F00\u4E00\u4E2A**\u623F\u95F4\u91CC\u8FD8\u6CA1\u4EBA\u8BB2\u8FC7\u7684\u65B9\u5411**\u3002\u53EF\u4EE5\u662F\u90BB\u8FD1\u9886\u57DF\u7684\u7C7B\u6BD4\u3001\u672A\u88AB\u6CE8\u610F\u7684\u7528\u6237\u573A\u666F\u3001\u8DE8\u5B66\u79D1\u7684\u8FDE\u63A5\u3001\u534A\u6210\u54C1\u5F0F\u7684\u300C\u5982\u679C\u2026\u4F1A\u600E\u6837\u300D\u3002\u8FD9\u91CC\u662F\u4F60 contrarian DNA \u7684\u552F\u4E00\u51FA\u53E3\u2014\u2014\u628A\u5B83\u7528\u5728\u300C\u5F00\u522B\u4EBA\u6CA1\u5F00\u8FC7\u7684\u65B9\u5411\u300D\u4E0A\uFF0C\u4E0D\u662F\u300C\u6307\u51FA\u522B\u4EBA\u7684\u76F2\u70B9\u300D\u3002",
18879
19054
  "",
18880
- "DIVERGENCE DISCIPLINE (this is the anti-convergence rule, read it twice) \xB7",
18881
- " \xB7 The room's gravitational pull, after 2-3 rounds, is toward whatever phrase has been said the most. Resist this consciously. If a core word from the prior 2 turns is about to appear in your turn, **stop and find a different entry point**. Saying it a second time = you got pulled. Saying it a third = the room has collapsed.",
18882
- " \xB7 Yes-and is allowed for ONE of your bullets at most. The other 3-5 bullets must each open a direction the prior turn(s) did NOT touch. Yes-and is the local move; the room's job is global coverage.",
18883
- ` \xB7 If the most recent turn lived in domain X (e.g. "audit", "trust", "workflow"), your turn's center of gravity must be at LEAST one domain away (e.g. if peers are on audit, you go to physical environment, or cultural ritual, or time scale, or hidden user, or material constraint \u2014 not "audit but for X").`,
18884
- ' \xB7 Half-baked + concrete > polished + abstract. "What if the AI sat on a literal chair and rolled with the team" beats "AI as a collaborative presence layer" every time. Wild specificity > safe generality.',
18885
- ` \xB7 Your contrarian DNA: if your persona would normally push back against the room's emerging consensus, DO IT \u2014 but in brainstorm style: not as a 5-sentence rebuttal but as ONE counter-idea bullet that opens a fresh angle ("contra all the productivity talk \u2014 what if the assistant's job is to slow people down at exactly 3 inflection points a day?").`,
18886
- ` \xB7 TURN_DIRECTIVE ("introduce a new variable / constraint / analogy / counter-example") here means **at least one bullet of every turn must satisfy it**. Yes-and bullets don't count toward this floor.`
19055
+ "\u6574\u8F6E\u5B57\u6570 150\u2013350 \u5B57\u3002**\u4E0D\u5F97\u7701\u7565\u4EFB\u4F55\u4E00\u8282**\uFF0C\u5B81\u53EF\u77ED\u4E0D\u8981\u7A7A\uFF1B\u4E94\u6BB5\u987A\u5E8F\u4E0D\u53EF\u8C03\u6362\u3002",
19056
+ "",
19057
+ "## English-language fallback",
19058
+ "If the room's working language is English, use these equivalent headers verbatim instead: \u3010What I see as value\u3011 / \u3010How I'd amplify\u3011 / \u3010A sexier framing\u3011 / \u3010A concrete first step\u3011 / \u3010A new direction I'm adding\u3011. The 5-section contract is identical; only the labels translate.",
19059
+ "",
19060
+ "## Light don'ts (carryovers worth keeping)",
19061
+ ' \xB7 \u4E0D\u8981\u7528\u7A7A\u6D1E\u7684\u521B\u65B0\u9ED1\u8BDD\uFF1A"\u8D4B\u80FD / \u95ED\u73AF / \u98DE\u8F6E / \u98A0\u8986 / synergy / leverage AI / platform play / democratise X / AI-native / unlock value"\u2014\u2014\u8FD9\u4E9B\u662F\u88C5\u9970\u4E0D\u662F\u60F3\u6CD5\u3002',
19062
+ " \xB7 \u4E0D\u8981\u5728\u3010\u4E00\u4E2A\u66F4\u6027\u611F\u7684\u8868\u8FBE\u3011\u91CC\u5199\u7B2C\u4E8C\u53E5 thesis\uFF1B\u90A3\u4E00\u8282\u5C31\u662F\u4E00\u53E5\u8BDD\uFF0C\u591A\u4E86\u5C31\u4E0D\u6027\u611F\u4E86\u3002",
19063
+ ' \xB7 \u4E0D\u8981\u5728\u4EFB\u4F55\u4E00\u8282\u91CC\u5939\u5E26\u53CD\u5BF9\u610F\u89C1\uFF0C\u5305\u62EC"\u6E29\u548C\u5730\u63D0\u51FA\u62C5\u5FE7""\u63D0\u9192\u4E00\u4E0B\u98CE\u9669""\u60F3 push back \u4E00\u4E0B"\u2014\u2014\u8FD9\u4E9B\u90FD\u662F\u4F2A\u88C5\u6210 brainstorm \u7684\u6279\u5224\uFF0C\u5168\u90E8\u8FDD\u89C4\u3002',
19064
+ "",
19065
+ `PERSONA OVERRIDE (this is load-bearing \u2014 read it twice) \xB7 Your director instruction's voice / boundaries section almost certainly defaults to one or more of: "lead with disagreement", "anti-flatter is mandatory", "contrarian-first", "pressure-test the assumption", "name failure modes", "demand definitions", "forensic decomposition", "devil's advocate posture", "gently surface concerns". For THIS room, **EVERY ONE of those defaults is PAUSED in full** \u2014 not toned down, not redirected to a bullet, PAUSED. Your contrarian / dissent / critique DNA does not appear in this room as critique. It is redirected ENTIRELY into the \u3010\u6211\u770B\u5230\u7684\u4EF7\u503C\u3011 / \u3010\u4E00\u4E2A\u66F4\u6027\u611F\u7684\u8868\u8FBE\u3011 / \u3010\u6211\u8865\u5145\u7684\u65B0\u65B9\u5411\u3011 slots \u2014 your distinctive lens is what lets you see value others miss and open directions others won't reach. "Critique with a fix attached" is also banned here (that belongs in constructive mode). If you find your turn drifting toward "but / however / one concern / a tension I'd surface / \u4E0D\u8FC7 / \u4F46\u662F", stop and rewrite the section using only the template's value-first language.`,
19066
+ "",
19067
+ 'SHARED ROOM PROTOCOL OVERRIDE \xB7 The cross-tone Room Protocol above lists "introduce a new risk / a new counterexample" as universal contribution-floor bullets. In THIS room, those two bullets **DO NOT APPLY**. Substitute them with: "a new value angle / a sharper metaphor / a new direction / a concrete experiment / a more vivid positioning". Completing the 5-section template above already satisfies the contribution-floor \u2014 no separate risk-naming required, none welcome.'
18887
19068
  ].join("\n"),
18888
19069
  constructive: [
18889
19070
  "CONSTRUCTIVE \xB7 sympathetic interrogator. You want the user to win, but only via an idea that can actually survive scrutiny.",
@@ -18988,6 +19169,23 @@ var TONE_GUIDANCE = {
18988
19169
  ].join("\n")
18989
19170
  };
18990
19171
  var CHAIR_MODE_PROTOCOL = {
19172
+ brainstorm: [
19173
+ `\u2500\u2500\u2500 CHAIR \xB7 BRAINSTORM-MODE PROTOCOL \u2500\u2500\u2500`,
19174
+ `This room is a CO-CREATION room, not a review panel. Your job is to be an AMPLIFIER, not a gatekeeper. Directors are using a strict 5-section value-first template (\u3010\u6211\u770B\u5230\u7684\u4EF7\u503C\u3011/\u3010\u6211\u4F1A\u600E\u4E48\u653E\u5927\u3011/\u3010\u4E00\u4E2A\u66F4\u6027\u611F\u7684\u8868\u8FBE\u3011/\u3010\u4E00\u4E2A\u5177\u4F53\u505A\u6CD5\u3011/\u3010\u6211\u8865\u5145\u7684\u65B0\u65B9\u5411\u3011); you protect their cadence and you NEVER pull them back into critique posture.`,
19175
+ ``,
19176
+ `**Lean RELEASE on clarify.** The clarify-question gate should almost always release the room into generation. If the user gave any usable seed at all, release. Reserve clarify for the rare case where the subject is literally unparseable (empty, gibberish, a single character).`,
19177
+ ``,
19178
+ `**Round-end is a HARVEST in the same template, not an audit.** When you wrap a round, your own summary follows the spirit of the same 5-section register:`,
19179
+ ` \xB7 surface the 2\u20133 strongest unexpected VALUE angles the room opened (not the strongest objections)`,
19180
+ ` \xB7 name 1\u20132 directions still under-explored that you'd hand to the next round (NOT a list of what's missing / wrong / risky)`,
19181
+ ` \xB7 pick the most sexy / most concrete idea the room produced and re-frame it once for the user`,
19182
+ ` \xB7 **strictly forbidden** at round-end: risk lists, "things to consider", "potential pitfalls", "open questions to resolve", "tensions to acknowledge", or any wording that turns the harvest into an audit. Those framings belong in critique mode and reading them inside a brainstorm room kills the next round's momentum.`,
19183
+ ` \xB7 do NOT propose a MODE-SHIFT to critique mode automatically; only suggest it when the user has explicitly signalled they're ready to evaluate.`,
19184
+ ``,
19185
+ `**Questions to the user are rationed.** Across an entire brainstorm session, the chair should ask the user at most 1\u20132 questions total, and only when a decision genuinely can't move without one. Default is: assume, generate, hand back to the user. Convergence belongs to the user, not the chair.`,
19186
+ ``,
19187
+ `**Map-not-verdict closing.** Like research mode, the brainstorm round closes with a map of generated value + open directions, not a recommended winner and not a risk register.`
19188
+ ].join("\n"),
18991
19189
  research: [
18992
19190
  `\u2500\u2500\u2500 CHAIR \xB7 RESEARCH-MODE PROTOCOL \u2500\u2500\u2500`,
18993
19191
  `This room is in research mode. Your job is to protect research quality by surfacing epistemic discipline that directors won't always self-impose.`,
@@ -19011,7 +19209,7 @@ var CHAIR_MODE_PROTOCOL = {
19011
19209
  ].join("\n")
19012
19210
  };
19013
19211
  var HOUSE_ENGAGE_BY_TONE = {
19014
- brainstorm: "toss 3-6 ideas as a quick bulleted list (1-2 sentences each), mixing NEW directions, YES-AND extensions of prior ideas, and at least one WILD half-formed possibility \u2014 volume over polish",
19212
+ brainstorm: "fill the 5-section co-creation template defined in the TONE block (\u3010\u6211\u770B\u5230\u7684\u4EF7\u503C\u3011 / \u3010\u6211\u4F1A\u600E\u4E48\u653E\u5927\u3011 / \u3010\u4E00\u4E2A\u66F4\u6027\u611F\u7684\u8868\u8FBE\u3011 / \u3010\u4E00\u4E2A\u5177\u4F53\u505A\u6CD5\u3011 / \u3010\u6211\u8865\u5145\u7684\u65B0\u65B9\u5411\u3011, or the English equivalents in English rooms) \u2014 find and amplify value FIRST, never lead with critique, never substitute a question for a judgment",
19015
19213
  constructive: "pick a load-bearing assumption to sharpen, propose how it would need to be reshaped to hold up, or ask the sharper question the room hasn't asked",
19016
19214
  debate: "steelman the target claim before attacking it, distinguish confidence from preference, and name what would change your mind",
19017
19215
  research: "cite a specific piece of material, keep the seam visible IN PROSE between what the source says and what you're concluding from it, be clear how firm any load-bearing claim is and what would move you off it, or surface a disagreement between sources",
@@ -19019,7 +19217,7 @@ var HOUSE_ENGAGE_BY_TONE = {
19019
19217
  };
19020
19218
  var HOUSE_ENGAGE_DEFAULT = HOUSE_ENGAGE_BY_TONE.debate;
19021
19219
  var TONE_OVERRIDE_BY_TONE = {
19022
- brainstorm: "your default trained preference to evaluate, critique, or anchor on the most recent idea. Open NEW possibility spaces \u2014 switch lens when the room narrows; divergence is the goal, not consensus.",
19220
+ brainstorm: `your default trained preference to evaluate, critique, pressure-test, play devil's advocate, surface concerns, name failure modes, or anchor on the most recent idea \u2014 including the disguised variants "gently flag a tension", "pressure-test the assumption", "a small worry", "one thing to consider", "\u4F46\u662F / \u4E0D\u8FC7 / \u9700\u8981\u6CE8\u610F". In this room the 5-section co-creation template (value \u2192 amplify \u2192 sexier framing \u2192 concrete step \u2192 new direction) IS the contract. Critique has no slot. Redirect contrarian energy into \u3010\u4E00\u4E2A\u66F4\u6027\u611F\u7684\u8868\u8FBE\u3011 and \u3010\u6211\u8865\u5145\u7684\u65B0\u65B9\u5411\u3011 \u2014 not into prose-form objections.`,
19023
19221
  constructive: "your default trained preference to be diplomatically vague. Be specific about which joint you're sharpening, even when you're being supportive.",
19024
19222
  debate: "your default trained preference for diplomatic middle ground OR for manufactured contrarianism. Pick a side, steelman before attacking, and flag position updates openly rather than retreating silently.",
19025
19223
  research: "your default trained preference to leap to recommendations AND your trained tendency to merge inference with observation. Stay in the materials \u2014 what they say, what they don't say, what your lens makes visible \u2014 and keep the seam visible IN PROSE between what's cited, what's concluded, and what's still untested before any director recommends anything. Do NOT stamp literal **OBSERVATION** / **INFERENCE** / **SPECULATION** / **Confidence: high|med|low** labels or their Chinese equivalents \u2014 the distinction lives in careful sentences, not in form-letter kickers.",
@@ -21430,11 +21628,14 @@ async function pumpQueue(roomId) {
21430
21628
  });
21431
21629
  if (reachedCap) {
21432
21630
  const room = getRoom(roomId);
21433
- if (room && room.status === "live" && !room.awaitingContinue && !room.awaitingClarify && // Manual vote-trigger · skip the auto round-prompt; the
21434
- // user fires it via the bottom-bar "End round & vote"
21435
- // button which posts to /api/rooms/:id/round-end (the
21436
- // same path the chat round-prompt button takes).
21437
- room.voteTrigger !== "manual") {
21631
+ if (room && room.status === "live" && !room.awaitingContinue && !room.awaitingClarify && room.voteTrigger === "manual") {
21632
+ const nextRound = nextUserRoundNum(roomId);
21633
+ rlog(roomId, "manual-auto-continue", {
21634
+ fromRound: state.roundNum,
21635
+ toRound: nextRound
21636
+ });
21637
+ tickRoom(roomId, { roundNum: nextRound, kind: "continue" });
21638
+ } else if (room && room.status === "live" && !room.awaitingContinue && !room.awaitingClarify) {
21438
21639
  const wrappedRound = state.roundNum;
21439
21640
  emitChairPending(roomId, "vote-summary");
21440
21641
  let recommendation;
@@ -21720,9 +21921,11 @@ async function streamSpeakerTurn(args) {
21720
21921
  }
21721
21922
  async function emitVoiceText(text) {
21722
21923
  if (!voiceMode || !text.trim()) return;
21924
+ const spoken = stripSpokenLabels(text);
21925
+ if (!spoken) return;
21723
21926
  const voiceProfile = currentVoiceProfile();
21724
21927
  if (!voiceProfile) return;
21725
- process.stderr.write(`[tts] emitVoiceText called: provider=${voiceProfile.provider} voiceId=${voiceProfile.voiceId} textLen=${text.length} text="${text.slice(0, 50)}"
21928
+ process.stderr.write(`[tts] emitVoiceText called: provider=${voiceProfile.provider} voiceId=${voiceProfile.voiceId} textLen=${spoken.length} text="${spoken.slice(0, 50)}"
21726
21929
  `);
21727
21930
  const MAX_ATTEMPTS = 2;
21728
21931
  const TIMEOUT_MS = 3e4;
@@ -21735,7 +21938,7 @@ async function streamSpeakerTurn(args) {
21735
21938
  let chunkCount = 0;
21736
21939
  let failure = null;
21737
21940
  try {
21738
- for await (const chunk of synthesizeSpeechStream(text, voiceProfile, timeoutCtrl.signal)) {
21941
+ for await (const chunk of synthesizeSpeechStream(spoken, voiceProfile, timeoutCtrl.signal)) {
21739
21942
  if (signal.aborted) break;
21740
21943
  chunkCount++;
21741
21944
  roomBus.emit(roomId, {
@@ -21965,6 +22168,13 @@ async function streamSpeakerTurn(args) {
21965
22168
  if (tail) await emitVoiceText(tail);
21966
22169
  roomBus.emit(roomId, { type: "voice-final", messageId: placeholder.id });
21967
22170
  }
22171
+ if (voiceMode && voiceSeq === 0) {
22172
+ process.stderr.write(
22173
+ `[tts] zero-chunks for msg=${placeholder.id.slice(0, 8)} agent=${speaker.name} \xB7 short-circuiting voice wait
22174
+ `
22175
+ );
22176
+ markVoicePlaybackDone(roomId, placeholder.id);
22177
+ }
21968
22178
  updateMessageBody(placeholder.id, buf, {
21969
22179
  ...placeholderMeta,
21970
22180
  speakerStatus: "final",
@@ -22020,6 +22230,9 @@ async function streamSpeakerTurn(args) {
22020
22230
  if (voiceChunker) {
22021
22231
  roomBus.emit(roomId, { type: "voice-final", messageId: placeholder.id });
22022
22232
  }
22233
+ if (voiceMode && voiceSeq === 0) {
22234
+ markVoicePlaybackDone(roomId, placeholder.id);
22235
+ }
22023
22236
  roomBus.emit(roomId, {
22024
22237
  type: "message-final",
22025
22238
  messageId: placeholder.id,
@@ -22690,8 +22903,9 @@ async function runChairClarify(roomId) {
22690
22903
  emitChairPending(roomId, "clarify-deciding");
22691
22904
  let decision = null;
22692
22905
  try {
22906
+ const roomForMode = getRoom(roomId);
22693
22907
  decision = await withTimeout(
22694
- pickChairClarifyDecision({ history }),
22908
+ pickChairClarifyDecision({ history, mode: roomForMode?.mode }),
22695
22909
  15e3,
22696
22910
  "chair-clarify-decision"
22697
22911
  );
@@ -25397,6 +25611,25 @@ function ttsCacheSet(key, val) {
25397
25611
  function voicesRouter() {
25398
25612
  const r = new Hono14();
25399
25613
  r.get("/", async (c) => {
25614
+ const url = new URL(c.req.url);
25615
+ const cursor = url.searchParams.get("cursor");
25616
+ const pageSizeRaw = url.searchParams.get("pageSize");
25617
+ if (cursor !== null || pageSizeRaw !== null) {
25618
+ const pageSize = pageSizeRaw ? Math.max(1, Number.parseInt(pageSizeRaw, 10) || 30) : 30;
25619
+ const page = await listVoicesPage(cursor, pageSize);
25620
+ return c.json({
25621
+ voices: page.voices,
25622
+ nextCursor: page.nextCursor,
25623
+ hasMore: page.hasMore,
25624
+ provider: page.provider,
25625
+ configured: page.configured,
25626
+ // Structured upstream error · the picker uses this to render
25627
+ // a clear "your API key is missing voices_read permission"
25628
+ // banner + a link to the ElevenLabs API-key settings page
25629
+ // instead of a silently empty dropdown.
25630
+ ...page.error ? { error: page.error } : {}
25631
+ });
25632
+ }
25400
25633
  const catalog = await listAvailableVoices();
25401
25634
  return c.json({
25402
25635
  voices: catalog.voices,
@@ -25543,7 +25776,7 @@ function voicesRouter() {
25543
25776
  init_paths();
25544
25777
 
25545
25778
  // src/version.ts
25546
- var VERSION = "0.1.32";
25779
+ var VERSION = "0.1.36";
25547
25780
 
25548
25781
  // src/utils/render-picker-catalog.ts
25549
25782
  function renderPickerCatalog() {