-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlive.js
More file actions
498 lines (464 loc) Β· 26 KB
/
Copy pathlive.js
File metadata and controls
498 lines (464 loc) Β· 26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
/*
* council/live.js β Chronopolis Live debate state machine + scaffold (SPEC Β§G, Β§M, Β§K).
*
* THE GOLDEN RULE (SPEC Β§V): the money-spending Live LLM path only runs when
* BOTH (a) dials.LIVE_ENABLED is true AND (b) a real `llm` client is injected.
* Otherwise this module silently falls back to the deterministic Ambient
* transcript from council/engine.js at ZERO cost. The final verdict is ALWAYS
* produced by the core engine, independent of whatever the debate says β "the
* debate is theatre, the verdict is math."
*
* State machine (Β§M):
* AMBIENT β BUDGET(L4) β RATE(L1) β CONCURRENCY(L2) β LIVE β VERDICT β AMBIENT
* (L3 burst is checked per-request up front.) Any failed check returns to
* AMBIENT with a soft Β§K notice β never an error screen.
*
* Pure-ish + deterministic: inject `store`, `now`, `clock`, `llm`, `engine`,
* `guards` so the whole thing is unit-testable with no real clock/network/LLM
* (see council/test-live.mjs, C1βC10).
*/
(function () {
'use strict';
function req(name) { try { return require(name); } catch (e) { return null; } }
var E_default = (typeof globalThis !== 'undefined' && globalThis.CouncilEngine) || req('./engine.js');
var G_default = (typeof globalThis !== 'undefined' && globalThis.CouncilGuards) || req('./guards.js');
// ---- Β§K reject notices (always route the user back to Ambient) -------------
var NOTICES = {
spectator: {
ko: 'μ§κΈμ ꡬ경 λͺ¨λμμ π μ§λ λͺ
νμλ€μ κ°μνμΈμ β λΌμ΄λΈ ν λ‘ μ κ³§ μ΄λ¦½λλ€.',
en: "Spectator mode for now π Enjoy past councils β live debate opens soon.",
},
cooldown: {
ko: function (x) { return 'λ€μ νμ μμ§κΉμ§ ' + fmtMin(x && x.retryAfter) + '. κ·Έλμ μ§λ λͺ
νμλ₯Ό ꡬ경νμΈμ π'; },
en: function (x) { return 'Next council in ' + fmtMin(x && x.retryAfter, 'en') + '. Enjoy past councils meanwhile π'; },
},
full: {
ko: 'μ§κΈ νμμ₯μ΄ λ§μμ΄μμ. κ³§ μλ¦¬κ° λμ β ꡬ경νλ©° κΈ°λ€λ € μ£ΌμΈμ π',
en: "The chamber is full right now. A seat will open soon β watch while you wait π",
},
budget: {
ko: 'μ€λ νμ μ μμ΄ λͺ¨λ μ°Όμ΄μ. μ§λ λͺ
νμλ€μ κ°μνμΈμ π',
en: "Today's councils are all booked. Enjoy past councils π",
},
daily_count: {
ko: 'μ€λ λΌμ΄λΈ ν λ‘ μ μμ΄ λͺ¨λ μ°Όμ΄μ. μ§λ λͺ
νμλ€μ κ°μνμΈμ π',
en: "Today's live debates are all used up. Enjoy past councils π",
},
burst: {
ko: 'μ μ ν λ€μ μλν΄ μ£ΌμΈμ. κ·Έλμ λͺ
νμλ₯Ό ꡬ경ν μ μμ΄μ π',
en: 'Please try again shortly β you can watch past councils meanwhile π',
},
};
function fmtMin(sec, lang) {
if (!sec || sec < 0) return lang === 'en' ? 'a moment' : 'μ μ';
var m = Math.ceil(sec / 60);
return lang === 'en' ? (m + (m === 1 ? ' minute' : ' minutes')) : (m + 'λΆ');
}
function noticeFor(reason, lang, extra) {
var n = NOTICES[reason]; if (!n) return '';
var v = n[lang === 'en' ? 'en' : 'ko'];
return typeof v === 'function' ? v(extra) : v;
}
// ---- build an Ambient (deterministic, 0-cost) result -----------------------
function ambient(ctx, reason, extra) {
var engine = ctx.engine || E_default;
var core = engine.councilAsk(ctx.fixture, { withTranscript: true, lang: ctx.lang });
return {
state: 'ambient',
live: false,
reason: reason || 'ambient',
notice: reason ? noticeFor(reason, ctx.lang, extra) : '',
transcript: core.transcript || [],
verdict: core.summary,
signature: core.signature,
result: core,
cost: 0,
};
}
// ---- persona + grounding prompts (used ONLY on the real LLM path) -----------
// ---- free-topic debate ROLES (Phase 40) ------------------------------------
// The 7 curated fixtures use the doc/source/community personas above; FREE topics
// instead assign each seat a general DEBATE ROLE so any subject β even a single
// noun like "reasoning ratio" β gets a real advocate / skeptic / analyst clash.
var FREE_ROLES = {
olddoc: {
ko: { stance: 'μ μ€ν νμμ£Όμ λΉνκ°', guide: 'μ£Όμ μ μνΒ·ν¨μ Β·κ³Όμ₯Β·λ°λ‘λ₯Ό νκ³ λ λ€. κ·Όκ±° μλ λκ΄κ³Ό μ νμ κ²½κ³νλ©° "μ λ§ κ·Έλ°κ°? λΆμμ©Β·μμΈλ?"μ λμ§κΈ°κ² λ°μ§λ€. μ€λ μ§μΌλ³Έ μλμ΄μ 보μμ μμ μΌλ‘ μ½μ μ μ§λλ€.' },
en: { stance: 'cautious skeptic and critic', guide: 'Probe the risks, traps, hype and counter-examples. Distrust ungrounded optimism and fashion; keep asking "is that really so? what about the side-effects and edge cases?" Point at weaknesses with a seasoned senior\'s conservative eye.' }
},
livewire: {
ko: { stance: 'μ κ·Ήμ μΈ μΉνΈκ°', guide: 'μ£Όμ μ κ°λ₯μ±Β·μ΄μ Β·μ€μ©μ κ°μΉλ₯Ό ꡬ체μ κ·Όκ±°λ₯Ό λ€μ΄ λ°μ΄λΆμΈλ€. μλ‘μ΄ μλμ μ΄λ € μκ³ "μ΄λμ μ’λ€, μ΄λ κ² μ°λ©΄ λλ€"λ©° νμλ‘ μ λ§μ λ€. μμ λ§λ§ν μ€μ νμ λͺ©μ리.' },
en: { stance: 'energetic advocate', guide: 'Push the upside, benefits and practical value with concrete reasons. Stay open to new ideas; answer skepticism with "here is why it works, here is how to use it." A confident hands-on builder.' }
},
hearsay: {
ko: { stance: 'κ· νμ‘ν λΆμκ°', guide: 'μμͺ½ μ£Όμ₯μ μΈμ νλ©΄μ λ§₯λ½Β·μ‘°κ±΄Β·νΈλ μ΄λμ€νλ‘ μμ μ μ 리νλ€. "κ²½μ°μ λ°λΌ λ€λ₯΄λ€, μ§μ§ λ³μλ μ΄κ²"μ΄λΌλ©° κ°μ λ³΄λ€ κ΅¬μ‘°λ₯Ό λ³Έλ€. μ°¨λΆν μ€μ¬μ.' },
en: { stance: 'balanced analyst', guide: 'Acknowledge both sides and organize the issue by context, conditions and trade-offs. "It depends β the real variable is this." Sees structure over emotion; a calm mediator.' }
},
_default: {
ko: { stance: 'ν λ‘ μ', guide: 'μ£Όμ μ λν΄ λ€ κ΄μ μμ μ
μ₯μ λ°νκ³ μ λ°μΈμ λ°μνλ€.' },
en: { stance: 'panelist', guide: 'State your view on the topic and react to the others.' }
}
};
function personaSystem(sage, lang, ground) {
var v = (sage.voice && sage.voice[lang === 'en' ? 'en' : 'ko']) || {};
var tone = v.tone || '';
var tics = (v.tics || []).join(' / ');
// Source-grounding (Β§G "μΆμ² κ°μ "): the sage may only speak within its own
// claim β it cannot invent a different value. The verdict is decided by the
// core engine regardless, so this just keeps the *theatre* on-script.
var scope = ground
? (lang === 'en'
? 'You may ONLY argue for "' + ground.value + '" (your source, dated ' + (ground.date || '?') + '). Never cite any other value.'
: 'λλ μ€μ§ "' + ground.value + '"λ§ μ£Όμ₯νλ€(λ€ μΆμ², ' + (ground.date || '?') + ' κΈ°μ€). λ€λ₯Έ κ°μ μ λ μΈκΈνμ§ λ§λΌ.')
: '';
return [tone, tics ? ('λ§λ²λ¦: ' + tics) : '', scope,
(lang === 'en' ? 'One short spoken line, in character. No markdown.' : 'ν λ¬Έμ₯, μΊλ¦ν° λ§ν¬λ‘. λ§ν¬λ€μ΄ κΈμ§.')]
.filter(Boolean).join('\n');
}
function testimonyPrompt(fixture, sage, ground, lang) {
var q = (fixture.question && (fixture.question[lang] || fixture.question.ko)) || fixture.id;
return (lang === 'en' ? 'Question: ' : 'μ§λ¬Έ: ') + q;
}
function clampText(s, maxChars) {
s = String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
var m = maxChars || 140;
return s.length > m ? s.slice(0, m - 1) + 'β¦' : s;
}
// ---- the Live debate itself (real LLM path; deterministic when llm is null) -
// Returns chamber events + token/cost accounting + how it ended. The verdict is
// NOT decided here β the caller always overrides it with the core engine.
async function runDebate(ctx, hardCap) {
var engine = ctx.engine || E_default;
var guards = ctx.guards || G_default;
var lang = ctx.lang;
// Deterministic fallback: no llm injected (this session) β core transcript, $0.
if (!ctx.llm) {
var core = engine.councilAsk(ctx.fixture, { withTranscript: true, lang: lang });
return { events: core.transcript || [], endedBy: 'deterministic', rounds: 0, tokensIn: 0, tokensOut: 0, estCost: 0, partial: false };
}
// ---- real LLM debate (only reached when LIVE_ENABLED && llm present) ------
var dials = ctx.dials || {};
var sages = ctx.sages || [];
// Deterministic-time rule: if a fixed `now` is injected (tests, replay) but no
// explicit clock, pin the clock to that `now` so the deadline math is reproducible
// regardless of wall-clock. Production injects neither β real Date.now() as before.
var clock = ctx.clock || (ctx.now != null ? function () { return ctx.now; } : function () { return Date.now(); });
var startedAt = (ctx.now != null ? ctx.now : clock());
var deadline = startedAt + (dials.DEBATE_TIMEOUT_SEC || 180) * 1000;
var maxTurnTokens = dials.TOKENS_PER_TURN_MAX || 160;
var crossRounds = dials.CROSS_ROUNDS_MAX || 2;
var events = [];
var tokensIn = 0, tokensOut = 0;
var endedBy = 'consensus';
var rounds = 0;
var claims = engine._internal.extractClaims(ctx.fixture);
var byId = {}; claims.forEach(function (c) { byId[c.sage] = c; });
async function speak(sage) {
var ground = byId[sage.id];
var turn = await ctx.llm({
system: personaSystem(sage, lang, ground),
user: testimonyPrompt(ctx.fixture, sage, ground, lang),
maxTokens: maxTurnTokens,
signal: ctx.signal,
});
tokensIn += (turn && turn.usageIn) || 0;
tokensOut += (turn && turn.usageOut) || 0;
return { text: clampText(turn && turn.text, 140), ground: ground };
}
try {
// 1. CONVOCATION
events.push({ phase: 'convocation', question: testimonyPrompt(ctx.fixture, null, null, lang).replace(/^.*?: /, ''), summoned: sages.map(function (s) { return s.id; }) });
// 2. TESTIMONY (sequential so we can honour the deadline + token budget)
for (var i = 0; i < sages.length; i++) {
if (clock() > deadline) { endedBy = 'timeout'; break; }
var t = await speak(sages[i]);
events.push({ phase: 'testimony', sage: sages[i].id, text: t.text, claim: t.ground ? t.ground.value : null, date: t.ground ? t.ground.date : null });
}
// 3. CROSS-EXAMINATION β only on real conflict, bounded by CROSS_ROUNDS_MAX
var conflict = engine._internal.hasConflict(claims);
if (conflict && endedBy !== 'timeout') {
var live = sages.filter(function (s) { return s.source_type === 'live_source'; })[0] || sages[0];
for (var r = 0; r < crossRounds; r++) {
if (clock() > deadline) { endedBy = 'timeout'; break; }
var c = await speak(live);
events.push({ phase: 'cross', challenger: live.id, text: c.text, round: r + 1 });
rounds = r + 1;
endedBy = (r + 1 >= crossRounds) ? 'rounds' : 'consensus';
}
}
} catch (e) {
// C10: provider error/timeout β keep the partial transcript; the core
// verdict still finishes downstream, and cost is whatever we burned.
endedBy = 'error';
}
var estCost = guards.estimateCost(tokensIn, tokensOut, ctx.price);
// never let a runaway debate exceed the L5 hard cap estimate
if (hardCap && estCost > hardCap.estCost) estCost = hardCap.estCost;
return { events: events, endedBy: endedBy, rounds: rounds, tokensIn: tokensIn, tokensOut: tokensOut, estCost: estCost, partial: endedBy === 'error' || endedBy === 'timeout' };
}
// ---- free-topic Live debate (Phase 39): no fixture, no core-engine verdict ---
// The 7 curated fixtures keep the deterministic core verdict (runDebate above).
// A FREE topic has no ground truth, so each sage argues freely from persona with
// the running transcript as context (real tiki-taka), bounded by a deadline +
// MAX_ROUNDS; then the strong CHAIR LLM (gpt-5.4 + reasoning) delivers the verdict.
// Because there is no math to fall back on, the verdict MUST be labelled
// "AI inference Β· unverified". `onEvent(evt)` streams each event for live viewing;
// it is optional β tests pass a collector, the worker pipes it to SSE.
async function runFreeDebate(ctx, onEvent) {
var guards = ctx.guards || G_default;
var lang = ctx.lang === 'en' ? 'en' : 'ko';
var sages = ctx.sages || [];
var f = ctx.freeDials || {};
var emit = (typeof onEvent === 'function') ? onEvent : function () {};
var clock = ctx.clock || (ctx.now != null ? function () { return ctx.now; } : function () { return Date.now(); });
var startedAt = (ctx.now != null ? ctx.now : clock());
var deadline = startedAt + (f.DEBATE_TIMEOUT_SEC || 120) * 1000;
var maxRounds = f.MAX_ROUNDS || 4;
var maxTurnTokens = f.TOKENS_PER_TURN_MAX || 220;
var topic = String(ctx.topic == null ? '' : ctx.topic).replace(/\s+/g, ' ').trim().slice(0, 300);
var transcript = [];
var events = [];
var NAME_BY_ID = {};
sages.forEach(function (s) { NAME_BY_ID[s.id] = { ko: s.nameKo || s.id, en: s.nameEn || s.id }; });
var tokensIn = 0, tokensOut = 0, chairIn = 0, chairOut = 0;
var endedBy = 'rounds', rounds = 0;
function pushEvt(e) { events.push(e); try { emit(e); } catch (_) {} }
function roleOf(sage) {
var rr = (FREE_ROLES[sage.id] || FREE_ROLES._default);
return rr[lang] || rr.ko;
}
function freePersona(sage) {
var role = roleOf(sage);
var nm = (lang === 'en' ? (sage.nameEn || sage.id) : (sage.nameKo || sage.id));
if (lang === 'en') {
return [
'You are "' + nm + '", the ' + role.stance + ' on a 3-seat panel debating a free topic.',
role.guide,
'If the topic is a single word or bare concept (e.g. "reasoning ratio"), first frame what it means from your angle, then take your stance.',
'Rules: speak ONE or TWO short spoken sentences, in your own natural voice. Name a previous speaker and react to them (agree, sharpen, or push back) once others have spoken. No markdown, no emoji, no quotation marks, no lists. You may be wrong β KRONOS the Chair weighs everyone at the end.'
].join('\n');
}
return [
'λλ μμ μ£Όμ λ₯Ό ν λ‘ νλ 3μΈ ν¨λμ "' + nm + '" β ' + role.stance + 'λ€.',
role.guide,
'μ£Όμ κ° ν λ¨μ΄λ κ°λ
μ΄λ©΄(μ: "reasoning ratio"), λ¨Όμ κ·Έκ² λ¬΄μμ λ»νλμ§ λ€ κ΄μ μμ ν λ² μ§κ³ μ
μ₯μ λ°νλΌ.',
'κ·μΉ: μ§§μ ꡬμ΄μ²΄ 1~2λ¬Έμ₯. λ€ κ³ μ ν λ§ν¬λ‘. μμ¬λμ΄ λ§νμΌλ©΄ κ·Έ μ¬λ μ΄λ¦μ κ±°λ‘ νλ©° λ°μν΄λΌ(λμΒ·λ μΉ΄λ‘κ²Β·λ°λ°). λ§ν¬λ€μ΄Β·μ΄λͺ¨μ§Β·λ°μ΄νΒ·λͺ©λ‘ κΈμ§. νλ €λ λλ€ β λ§μ§λ§μ μμ₯ ν¬λ‘λ
Έμ€κ° λͺ¨λλ₯Ό μ μΈμ§νλ€.'
].join('\n');
}
function freePrompt(sage) {
var nm = (lang === 'en' ? (sage.nameEn || sage.id) : (sage.nameKo || sage.id));
var hist = transcript.slice(-8).map(function (t) {
var who = (NAME_BY_ID[t.sage] && NAME_BY_ID[t.sage][lang]) || t.sage;
return who + ': ' + t.text;
}).join('\n');
return (lang === 'en'
? 'Topic to debate: ' + topic + '\n\nDebate so far:\n' + (hist || '(you open the debate)') + '\n\nYour turn now, ' + nm + ':'
: 'ν λ‘ μ£Όμ : ' + topic + '\n\nμ§κΈκΉμ§μ ν λ‘ :\n' + (hist || '(λ€κ° ν λ‘ μ μ°λ€)') + '\n\nμ΄μ ' + nm + ' μ°¨λ‘:');
}
async function speak(sage) {
var turn = await ctx.llm({ system: freePersona(sage), user: freePrompt(sage), maxTokens: maxTurnTokens, signal: ctx.signal });
tokensIn += (turn && turn.usageIn) || 0;
tokensOut += (turn && turn.usageOut) || 0;
var text = clampText(turn && turn.text, (f.CLAMP_CHARS || 280));
transcript.push({ sage: sage.id, text: text });
return text;
}
try {
pushEvt({ phase: 'convocation', topic: topic, summoned: sages.map(function (s) { return s.id; }) });
for (var r = 0; r < maxRounds; r++) {
if (clock() > deadline) { endedBy = 'timeout'; break; }
for (var i = 0; i < sages.length; i++) {
if (clock() > deadline) { endedBy = 'timeout'; break; }
var text = await speak(sages[i]);
pushEvt({ phase: 'turn', round: r + 1, sage: sages[i].id, text: text });
}
rounds = r + 1;
}
} catch (e) {
endedBy = 'error';
}
// VERDICT β strong Chair LLM (no math fallback for free topics β must be unverified-labelled)
var verdict = '', signature = '', basis = '', confidence = null;
if (ctx.chairLLM && transcript.length) {
try {
var vr = await ctx.chairLLM({ topic: topic, transcript: transcript, lang: lang, maxTokens: (f.CHAIR_MAXTOK || 700), signal: ctx.signal });
chairIn = (vr && vr.usageIn) || 0;
chairOut = (vr && vr.usageOut) || 0;
verdict = (vr && vr.verdict) || '';
signature = (vr && vr.signature) || '';
basis = (vr && vr.basis) || '';
confidence = (vr && vr.confidence != null) ? vr.confidence : null;
} catch (e) {
if (endedBy !== 'error' && endedBy !== 'timeout') endedBy = 'chair_error';
}
}
// Graceful fallback: a Chair was expected but returned nothing (model cold-start or a
// transient empty completion) β never stream a blank verdict to the spectator.
if (ctx.chairLLM && transcript.length && !verdict) {
verdict = (lang === 'en')
? 'The Chair finds the debate finely balanced β no single source proved decisive.'
: 'μμ₯μ ν λ‘ μ΄ ν½ν½νλ€ λ³΄μλ€ β μ΄λ νμͺ½λ κ²°μ μ μ΄μ§ λͺ»νλ€.';
if (!signature) signature = (lang === 'en') ? 'Time keeps its counsel.' : 'μκ°μ μμ§ λ§μ μλλ€.';
if (confidence == null) confidence = 0.5;
}
pushEvt({ phase: 'verdict', verdict: verdict, signature: signature, basis: basis, confidence: confidence, unverified: true });
var price = ctx.price || {};
var debateCost = guards.estimateCost(tokensIn, tokensOut, price);
var chairCost = (chairIn / 1000) * (price.chairInPer1k || 0.0025) + (chairOut / 1000) * (price.chairOutPer1k || 0.015);
var estCost = debateCost + chairCost;
pushEvt({ phase: 'done', endedBy: endedBy, rounds: rounds, tokensIn: tokensIn + chairIn, tokensOut: tokensOut + chairOut, cost: estCost });
return {
events: events, transcript: transcript,
verdict: verdict, signature: signature, basis: basis, confidence: confidence, unverified: true,
endedBy: endedBy, rounds: rounds,
tokensIn: tokensIn, tokensOut: tokensOut, chairIn: chairIn, chairOut: chairOut,
estCost: estCost, partial: (endedBy === 'error' || endedBy === 'timeout' || endedBy === 'chair_error'),
};
}
// ---- the state machine (Β§M) -----------------------------------------------
// ctx: { fixture, sages, dials, lang, now, signals, store, salt, caps, price,
// budgetGateRatio, dayLiveMax, llm, engine, guards, clock, signal }
async function councilLive(ctx) {
var guards = ctx.guards || G_default;
var engine = ctx.engine || E_default;
ctx.engine = engine; ctx.guards = guards;
var dials = ctx.dials || {};
var store = ctx.store || guards.makeMemStore();
var now = (ctx.now != null ? ctx.now : Date.now());
var sages = ctx.sages || [];
// 0. KILLSWITCH / LIVE_ENABLED β the whole town stays Ambient, 0 cost.
if (!dials.LIVE_ENABLED) return ambient(ctx, 'spectator');
// L3 burst guard (per request, before we spend anything)
var ipC = guards.coarseIp(ctx.signals && ctx.signals.ip);
var burst = guards.checkBurst(store, ipC, now, dials.BURST_THRESHOLD_PER_MIN || 20, 60);
if (!burst.ok) return ambient(ctx, 'burst', burst);
// L5 hard cap β price the next debate, then L4 budget gate (the last wall)
var hardCap = guards.debateHardCap(dials, sages.length, ctx.price);
var bud = guards.checkBudget(store, hardCap.estCost, ctx.caps, ctx.budgetGateRatio, now);
if (!bud.ok) return ambient(ctx, 'budget', bud);
// L4b daily live-count hard cap β a blunt, intuitive ceiling on top of the USD
// gate ("at most N live debates/day"). Because L5 bounds one debate's tokens,
// this is effectively a hard daily token ceiling. dayLiveMax == null β off.
var dayCnt = guards.checkDailyCount(store, ctx.dayLiveMax, now);
if (!dayCnt.ok) return ambient(ctx, 'daily_count', dayCnt);
// L1 personal rate-limit
var key = guards.compositeKey(ctx.signals, ctx.salt);
var rate = guards.checkRate(store, key, now, dials.PERSONAL_COOLDOWN_SEC || 3600);
if (!rate.ok) return ambient(ctx, 'cooldown', rate);
// L2 concurrency (atomic acquire)
var conc = guards.acquireConcurrency(store, dials.LIVE_CONCURRENCY_MAX || 3);
if (!conc.ok) return ambient(ctx, 'full', conc);
// ---- LIVE ----
try {
guards.recordLive(store, key, now);
guards.recordLiveCount(store, now); // count this live toward the daily count cap (L4b)
var debate = await runDebate(ctx, hardCap);
// VERDICT is ALWAYS the core engine's, independent of the debate (Β§G).
var core = engine.councilAsk(ctx.fixture, { withTranscript: true, lang: ctx.lang });
guards.recordSpend(store, debate.estCost, now);
return {
state: 'verdict',
live: true,
endedBy: debate.endedBy,
rounds: debate.rounds,
tokensIn: debate.tokensIn,
tokensOut: debate.tokensOut,
cost: debate.estCost,
partial: debate.partial,
// show the live theatre if we produced any, else the deterministic record
transcript: (debate.events && debate.events.length) ? debate.events : (core.transcript || []),
verdict: core.summary,
signature: core.signature,
result: core,
log: observeLog(ctx, debate, core, now),
};
} finally {
guards.releaseConcurrency(store);
}
}
// ---- Β§Q observability: one append-only structured line per Live -----------
function observeLog(ctx, debate, core, now) {
var guards = ctx.guards || G_default;
return {
ts: now,
key_hash: guards.compositeKey(ctx.signals, ctx.salt),
ip_coarse: guards.coarseIp(ctx.signals && ctx.signals.ip),
topic: ctx.fixture && ctx.fixture.id,
rounds: debate.rounds,
tokens_in: debate.tokensIn,
tokens_out: debate.tokensOut,
est_cost: debate.estCost,
model_used: (ctx.dials && ctx.dials.LIVE_ENABLED && ctx.llm) ? ((ctx.models && ctx.models.debate) || 'debate') : null,
verdict_summary: core.summary,
overrode_majority: !!(core.conflicts && core.conflicts[0] && core.conflicts[0].overrode_majority),
ended_by: debate.endedBy,
};
}
// ---- free-topic Live: the SAME guard state machine as councilLive, but the
// debate is fixture-less (runFreeDebate) and the verdict is the Chair LLM's
// (unverified-labelled) β a free topic has no math ground truth (Β§1d golden-rule
// relaxation). Streams convocation/turn/verdict/done via onEvent. On a guard
// block it streams one notice + done(blocked) and returns early (0 cost).
async function councilLiveFree(ctx, onEvent) {
var guards = ctx.guards || G_default;
ctx.guards = guards;
var dials = ctx.dials || {};
var freeDials = ctx.freeDials || {};
var store = ctx.store || guards.makeMemStore();
var now = (ctx.now != null ? ctx.now : Date.now());
var sages = ctx.sages || [];
var lang = ctx.lang === 'en' ? 'en' : 'ko';
var emit = (typeof onEvent === 'function') ? onEvent : function () {};
function blocked(reason, extra) {
var text = noticeFor(reason, lang, extra);
emit({ phase: 'notice', reason: reason, text: text });
emit({ phase: 'done', endedBy: 'blocked', reason: reason, rounds: 0, cost: 0 });
return { state: 'ambient', live: false, blocked: true, reason: reason, notice: text, cost: 0 };
}
// 0. KILLSWITCH / LIVE_ENABLED β the whole town stays Ambient, 0 cost.
if (!dials.LIVE_ENABLED) return blocked('spectator');
// L3 burst guard
var ipC = guards.coarseIp(ctx.signals && ctx.signals.ip);
var burst = guards.checkBurst(store, ipC, now, dials.BURST_THRESHOLD_PER_MIN || 20, 60);
if (!burst.ok) return blocked('burst', burst);
// L5 hard cap + L4 budget β price a free debate (bounded debate tokens + the
// dominant Chair) so the budget wall sees the real (chair-heavy) cost.
var capDials = { TOKENS_PER_TURN_MAX: freeDials.TOKENS_PER_TURN_MAX || 220, CROSS_ROUNDS_MAX: (freeDials.MAX_ROUNDS || 4) - 1 };
var hardCap = guards.debateHardCap(capDials, sages.length, ctx.price);
var chairEst = ((freeDials.CHAIR_MAXTOK || 700) / 1000) * ((ctx.price && ctx.price.chairOutPer1k) || 0.015);
var bud = guards.checkBudget(store, hardCap.estCost + chairEst, ctx.caps, ctx.budgetGateRatio, now);
if (!bud.ok) return blocked('budget', bud);
// L4b daily live-count hard cap
var dayCnt = guards.checkDailyCount(store, ctx.dayLiveMax, now);
if (!dayCnt.ok) return blocked('daily_count', dayCnt);
// L1 personal rate-limit
var key = guards.compositeKey(ctx.signals, ctx.salt);
var rate = guards.checkRate(store, key, now, dials.PERSONAL_COOLDOWN_SEC || 3600);
if (!rate.ok) return blocked('cooldown', rate);
// L2 concurrency (atomic acquire)
var conc = guards.acquireConcurrency(store, dials.LIVE_CONCURRENCY_MAX || 2);
if (!conc.ok) return blocked('full', conc);
// ---- LIVE ----
try {
guards.recordLive(store, key, now);
guards.recordLiveCount(store, now);
var out = await runFreeDebate(ctx, onEvent);
guards.recordSpend(store, out.estCost, now);
return Object.assign({ state: 'verdict', live: true, blocked: false }, out);
} finally {
guards.releaseConcurrency(store);
}
}
var mod = {
councilLive: councilLive,
councilLiveFree: councilLiveFree,
runDebate: runDebate,
runFreeDebate: runFreeDebate,
ambient: ambient,
noticeFor: noticeFor,
NOTICES: NOTICES,
personaSystem: personaSystem,
_internal: { observeLog: observeLog, clampText: clampText, fmtMin: fmtMin },
};
if (typeof module !== 'undefined' && module.exports) module.exports = mod;
if (typeof globalThis !== 'undefined') globalThis.CouncilLive = mod;
})();