privateboard 0.1.7 → 0.1.9
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 +1423 -46
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +195 -0
- package/public/agent-profile.css +525 -0
- package/public/agent-profile.js +278 -1
- package/public/app.js +634 -118
- package/public/home.html +389 -17
- package/public/index.html +325 -130
- package/public/magazine.html +1685 -0
- package/public/newspaper.html +1892 -0
- package/public/ppt.html +2623 -0
- package/public/report.html +366 -52
- package/public/room-settings.css +40 -4
- package/public/room-settings.js +44 -2
- package/public/user-settings.css +117 -68
- package/public/user-settings.js +77 -46
package/dist/cli.js
CHANGED
|
@@ -376,6 +376,77 @@ var init_usage_daily = __esm({
|
|
|
376
376
|
}
|
|
377
377
|
});
|
|
378
378
|
|
|
379
|
+
// src/storage/migrations/025_brief_mode.sql
|
|
380
|
+
var brief_mode_default;
|
|
381
|
+
var init_brief_mode = __esm({
|
|
382
|
+
"src/storage/migrations/025_brief_mode.sql"() {
|
|
383
|
+
brief_mode_default = "-- 025_brief_mode \xB7 add `mode` column to briefs.\n--\n-- Briefs now come in two flavors:\n-- \xB7 'research-note' (default \xB7 the existing markdown-body research\n-- dossier with composer-picked components and a spine-rendered\n-- report.html view)\n-- \xB7 'bento' (new \xB7 a single-page infographic with fixed structure \xB7\n-- header band, 3-milestone timeline, ranked-bars / verification /\n-- talking-points sidebars, conclusion band \xB7 rendered by bento.html\n-- from the BentoScaffold JSON persisted in body_json)\n--\n-- Legacy rows pre-dating bento are research-note by definition; the\n-- DEFAULT clause backfills them. The column is NOT NULL so the\n-- pipeline can branch on it without an `?? \"research-note\"` fallback\n-- at every read site.\n--\n-- For 'bento' rows the body_md column is empty (or carries a plain-\n-- text fallback) and body_json holds the structured BentoScaffold; the\n-- spine / components_json / composer_rationale columns are unused (the\n-- bento has fixed shape, the composer doesn't run for it).\n\nALTER TABLE briefs ADD COLUMN mode TEXT NOT NULL DEFAULT 'research-note';\n";
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// src/storage/migrations/026_memory_metabolism.sql
|
|
388
|
+
var memory_metabolism_default;
|
|
389
|
+
var init_memory_metabolism = __esm({
|
|
390
|
+
"src/storage/migrations/026_memory_metabolism.sql"() {
|
|
391
|
+
memory_metabolism_default = "-- Memory metabolism \xB7 Sleep / Dreaming Mode infrastructure (Phase 1).\n--\n-- Three forward-compatible additions to agent_memories that let a\n-- periodic \"dream\" pass decide which memories to keep, decay, or\n-- promote to long-term. Phase 1 only uses `usage_count` /\n-- `last_used_at` (decay heuristic) and `tier` (default 'short' for\n-- everything pre-existing). The remaining columns (superseded_by,\n-- consolidated_from, provenance_rooms) plus the agent_dreams audit\n-- table land in the Phase 2 migration.\n--\n-- Rationale per column:\n-- \xB7 tier \xB7 'short' / 'long' \xB7 stable cross-room patterns\n-- promote out of the recency cap (tier='long' is\n-- always injected into prompts; 'short' goes\n-- through the top-N recency window).\n-- \xB7 usage_count \xB7 how many times this memory has been injected\n-- into a director's prompt. Memories that ARE\n-- being read escape the decay sweep \u2014 only\n-- genuinely-forgotten rows get culled.\n-- \xB7 last_used_at \xB7 timestamp of most recent injection. Lets\n-- future tier-promotion rules consider \"freshness\n-- of relevance\" separately from \"freshness of\n-- creation.\"\n\nALTER TABLE agent_memories ADD COLUMN tier TEXT NOT NULL DEFAULT 'short';\nALTER TABLE agent_memories ADD COLUMN usage_count INTEGER NOT NULL DEFAULT 0;\nALTER TABLE agent_memories ADD COLUMN last_used_at INTEGER;\n\n-- Tier-aware retrieval \xB7 listTier(agentId, tier) reads through this\n-- index when memoriesForContext composes the prompt-injection set.\nCREATE INDEX IF NOT EXISTS idx_agent_memories_tier\n ON agent_memories(agent_id, tier, pinned DESC, created_at DESC);\n";
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// src/storage/migrations/027_memory_metabolism_p2.sql
|
|
396
|
+
var memory_metabolism_p2_default;
|
|
397
|
+
var init_memory_metabolism_p2 = __esm({
|
|
398
|
+
"src/storage/migrations/027_memory_metabolism_p2.sql"() {
|
|
399
|
+
memory_metabolism_p2_default = `-- Memory metabolism \xB7 Phase 2 schema \xB7 supersession / consolidation
|
|
400
|
+
-- audit + dream-cycle log.
|
|
401
|
+
--
|
|
402
|
+
-- Three additions to agent_memories let the LLM dream pipeline:
|
|
403
|
+
-- \xB7 merge near-duplicates without losing the originals (the canonical
|
|
404
|
+
-- merged memory points back at its sources via consolidated_from;
|
|
405
|
+
-- each source is marked superseded_by \u2192 the merged row).
|
|
406
|
+
-- \xB7 resolve contradictions by superseding the older claim with the
|
|
407
|
+
-- newer one (audit pointer survives, prompt-injection filter
|
|
408
|
+
-- drops it).
|
|
409
|
+
-- \xB7 weight stable cross-room patterns for promotion to tier='long'
|
|
410
|
+
-- via a count of distinct rooms that reinforced the memory.
|
|
411
|
+
--
|
|
412
|
+
-- The agent_dreams table is the audit log \xB7 one row per cycle so we
|
|
413
|
+
-- can surface "last dream 2h ago \xB7 dropped 3, merged 4" in the UI
|
|
414
|
+
-- and grep stderr-equivalent metrics out of the DB.
|
|
415
|
+
--
|
|
416
|
+
-- Phase 1 columns (tier / usage_count / last_used_at) were added in
|
|
417
|
+
-- migration 026; this migration assumes those are present.
|
|
418
|
+
|
|
419
|
+
ALTER TABLE agent_memories ADD COLUMN superseded_by TEXT REFERENCES agent_memories(id) ON DELETE SET NULL;
|
|
420
|
+
ALTER TABLE agent_memories ADD COLUMN consolidated_from TEXT; -- JSON array of source memory ids
|
|
421
|
+
ALTER TABLE agent_memories ADD COLUMN provenance_rooms INTEGER NOT NULL DEFAULT 1;
|
|
422
|
+
|
|
423
|
+
-- Index on (agent, superseded) so retrieval can quickly skip
|
|
424
|
+
-- consolidated/superseded rows. Also benefits "list forgotten"
|
|
425
|
+
-- audit views.
|
|
426
|
+
CREATE INDEX IF NOT EXISTS idx_agent_memories_superseded
|
|
427
|
+
ON agent_memories(agent_id, superseded_by);
|
|
428
|
+
|
|
429
|
+
CREATE TABLE IF NOT EXISTS agent_dreams (
|
|
430
|
+
id TEXT PRIMARY KEY,
|
|
431
|
+
agent_id TEXT NOT NULL,
|
|
432
|
+
started_at INTEGER NOT NULL,
|
|
433
|
+
finished_at INTEGER,
|
|
434
|
+
before_count INTEGER NOT NULL,
|
|
435
|
+
after_count INTEGER,
|
|
436
|
+
decayed INTEGER NOT NULL DEFAULT 0,
|
|
437
|
+
merged INTEGER NOT NULL DEFAULT 0,
|
|
438
|
+
promoted INTEGER NOT NULL DEFAULT 0,
|
|
439
|
+
superseded INTEGER NOT NULL DEFAULT 0,
|
|
440
|
+
notes TEXT,
|
|
441
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
CREATE INDEX IF NOT EXISTS idx_dreams_agent_recent
|
|
445
|
+
ON agent_dreams(agent_id, started_at DESC);
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
379
450
|
// src/storage/db.ts
|
|
380
451
|
var db_exports = {};
|
|
381
452
|
__export(db_exports, {
|
|
@@ -457,6 +528,9 @@ var init_db = __esm({
|
|
|
457
528
|
init_intensity_brutal_to_terse();
|
|
458
529
|
init_brief_assets();
|
|
459
530
|
init_usage_daily();
|
|
531
|
+
init_brief_mode();
|
|
532
|
+
init_memory_metabolism();
|
|
533
|
+
init_memory_metabolism_p2();
|
|
460
534
|
MIGRATIONS = [
|
|
461
535
|
{ name: "001_init.sql", sql: init_default },
|
|
462
536
|
{ name: "002_default_opus.sql", sql: default_opus_default },
|
|
@@ -481,7 +555,10 @@ var init_db = __esm({
|
|
|
481
555
|
{ name: "021_notes.sql", sql: notes_default },
|
|
482
556
|
{ name: "022_intensity_brutal_to_terse.sql", sql: intensity_brutal_to_terse_default },
|
|
483
557
|
{ name: "023_brief_assets.sql", sql: brief_assets_default },
|
|
484
|
-
{ name: "024_usage_daily.sql", sql: usage_daily_default }
|
|
558
|
+
{ name: "024_usage_daily.sql", sql: usage_daily_default },
|
|
559
|
+
{ name: "025_brief_mode.sql", sql: brief_mode_default },
|
|
560
|
+
{ name: "026_memory_metabolism.sql", sql: memory_metabolism_default },
|
|
561
|
+
{ name: "027_memory_metabolism_p2.sql", sql: memory_metabolism_p2_default }
|
|
485
562
|
];
|
|
486
563
|
_db = null;
|
|
487
564
|
}
|
|
@@ -820,6 +897,10 @@ function updateAgent(id, patch) {
|
|
|
820
897
|
const json = patch.ability && Object.keys(patch.ability).length > 0 ? JSON.stringify(patch.ability) : null;
|
|
821
898
|
values.push(json);
|
|
822
899
|
}
|
|
900
|
+
if (typeof patch.isPinned === "boolean") {
|
|
901
|
+
fields.push("is_pinned = ?");
|
|
902
|
+
values.push(patch.isPinned ? 1 : 0);
|
|
903
|
+
}
|
|
823
904
|
if (fields.length === 0) return getAgent(id);
|
|
824
905
|
fields.push("updated_at = ?");
|
|
825
906
|
values.push(Date.now());
|
|
@@ -1764,12 +1845,22 @@ function newId(len = 12) {
|
|
|
1764
1845
|
}
|
|
1765
1846
|
|
|
1766
1847
|
// src/storage/memories.ts
|
|
1767
|
-
var SELECT_COLS2 = "id, agent_id, content, kind, source, source_room, confidence, pinned, created_at, updated_at";
|
|
1848
|
+
var SELECT_COLS2 = "id, agent_id, content, kind, source, source_room, confidence, pinned, created_at, updated_at, tier, usage_count, last_used_at, superseded_by, consolidated_from, provenance_rooms";
|
|
1768
1849
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["fact", "observation", "preference", "goal"]);
|
|
1769
1850
|
var ALLOWED_SOURCES = /* @__PURE__ */ new Set(["extracted", "user_added", "user_pinned"]);
|
|
1851
|
+
var ALLOWED_TIERS = /* @__PURE__ */ new Set(["short", "long"]);
|
|
1770
1852
|
function mapRow2(row) {
|
|
1771
1853
|
const kind = ALLOWED_KINDS.has(row.kind) ? row.kind : "fact";
|
|
1772
1854
|
const source = ALLOWED_SOURCES.has(row.source) ? row.source : "extracted";
|
|
1855
|
+
const tier = ALLOWED_TIERS.has(row.tier) ? row.tier : "short";
|
|
1856
|
+
let consolidatedFrom = null;
|
|
1857
|
+
if (row.consolidated_from) {
|
|
1858
|
+
try {
|
|
1859
|
+
const parsed = JSON.parse(row.consolidated_from);
|
|
1860
|
+
if (Array.isArray(parsed)) consolidatedFrom = parsed.filter((x) => typeof x === "string");
|
|
1861
|
+
} catch {
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1773
1864
|
return {
|
|
1774
1865
|
id: row.id,
|
|
1775
1866
|
agentId: row.agent_id,
|
|
@@ -1780,13 +1871,20 @@ function mapRow2(row) {
|
|
|
1780
1871
|
confidence: row.confidence,
|
|
1781
1872
|
pinned: row.pinned === 1,
|
|
1782
1873
|
createdAt: row.created_at,
|
|
1783
|
-
updatedAt: row.updated_at
|
|
1874
|
+
updatedAt: row.updated_at,
|
|
1875
|
+
tier,
|
|
1876
|
+
usageCount: row.usage_count ?? 0,
|
|
1877
|
+
lastUsedAt: row.last_used_at,
|
|
1878
|
+
supersededBy: row.superseded_by,
|
|
1879
|
+
consolidatedFrom,
|
|
1880
|
+
provenanceRooms: row.provenance_rooms ?? 1
|
|
1784
1881
|
};
|
|
1785
1882
|
}
|
|
1786
|
-
function listMemoriesForAgent(agentId) {
|
|
1883
|
+
function listMemoriesForAgent(agentId, opts = {}) {
|
|
1884
|
+
const where = opts.includeSuperseded ? "WHERE agent_id = ?" : "WHERE agent_id = ? AND superseded_by IS NULL";
|
|
1787
1885
|
const rows = getDb().prepare(
|
|
1788
1886
|
`SELECT ${SELECT_COLS2} FROM agent_memories
|
|
1789
|
-
|
|
1887
|
+
${where}
|
|
1790
1888
|
ORDER BY pinned DESC, created_at DESC`
|
|
1791
1889
|
).all(agentId);
|
|
1792
1890
|
return rows.map(mapRow2);
|
|
@@ -1794,8 +1892,52 @@ function listMemoriesForAgent(agentId) {
|
|
|
1794
1892
|
function memoriesForContext(agentId, recentCap = 5) {
|
|
1795
1893
|
const all = listMemoriesForAgent(agentId);
|
|
1796
1894
|
const pinned = all.filter((m) => m.pinned);
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1895
|
+
const longTier = all.filter((m) => !m.pinned && m.tier === "long");
|
|
1896
|
+
const shortTier = all.filter((m) => !m.pinned && m.tier === "short").slice(0, recentCap);
|
|
1897
|
+
return [...pinned, ...longTier, ...shortTier];
|
|
1898
|
+
}
|
|
1899
|
+
function listTierForAgent(agentId, tier) {
|
|
1900
|
+
const rows = getDb().prepare(
|
|
1901
|
+
`SELECT ${SELECT_COLS2} FROM agent_memories
|
|
1902
|
+
WHERE agent_id = ? AND tier = ? AND superseded_by IS NULL
|
|
1903
|
+
ORDER BY pinned DESC, created_at DESC`
|
|
1904
|
+
).all(agentId, tier);
|
|
1905
|
+
return rows.map(mapRow2);
|
|
1906
|
+
}
|
|
1907
|
+
function bumpUsage(memoryIds) {
|
|
1908
|
+
if (!memoryIds.length) return;
|
|
1909
|
+
const db = getDb();
|
|
1910
|
+
const now = Date.now();
|
|
1911
|
+
const stmt = db.prepare(
|
|
1912
|
+
`UPDATE agent_memories
|
|
1913
|
+
SET usage_count = usage_count + 1,
|
|
1914
|
+
last_used_at = ?
|
|
1915
|
+
WHERE id = ?`
|
|
1916
|
+
);
|
|
1917
|
+
const tx = db.transaction((ids) => {
|
|
1918
|
+
for (const id of ids) stmt.run(now, id);
|
|
1919
|
+
});
|
|
1920
|
+
tx(memoryIds);
|
|
1921
|
+
}
|
|
1922
|
+
function decayShortTermMemories(agentId, thresholds = {}) {
|
|
1923
|
+
const minAgeMs = thresholds.minAgeMs ?? 30 * 24 * 60 * 60 * 1e3;
|
|
1924
|
+
const maxConfidence = thresholds.maxConfidence ?? 0.5;
|
|
1925
|
+
const maxUsage = thresholds.maxUsage ?? 0;
|
|
1926
|
+
const ageCutoff = Date.now() - minAgeMs;
|
|
1927
|
+
const r = getDb().prepare(
|
|
1928
|
+
`DELETE FROM agent_memories
|
|
1929
|
+
WHERE agent_id = ?
|
|
1930
|
+
AND tier = 'short'
|
|
1931
|
+
AND pinned = 0
|
|
1932
|
+
AND created_at < ?
|
|
1933
|
+
AND confidence < ?
|
|
1934
|
+
AND usage_count <= ?`
|
|
1935
|
+
).run(agentId, ageCutoff, maxConfidence, maxUsage);
|
|
1936
|
+
return r.changes ?? 0;
|
|
1937
|
+
}
|
|
1938
|
+
function countMemoriesForAgent(agentId) {
|
|
1939
|
+
const row = getDb().prepare(`SELECT COUNT(*) AS n FROM agent_memories WHERE agent_id = ?`).get(agentId);
|
|
1940
|
+
return row?.n ?? 0;
|
|
1799
1941
|
}
|
|
1800
1942
|
function insertMemory(input) {
|
|
1801
1943
|
const db = getDb();
|
|
@@ -1847,6 +1989,109 @@ function deleteMemory(id) {
|
|
|
1847
1989
|
function isMemoryKind(v) {
|
|
1848
1990
|
return ALLOWED_KINDS.has(v);
|
|
1849
1991
|
}
|
|
1992
|
+
function markSuperseded(memoryIds, supersedingId) {
|
|
1993
|
+
if (!memoryIds.length) return 0;
|
|
1994
|
+
const db = getDb();
|
|
1995
|
+
const now = Date.now();
|
|
1996
|
+
const stmt = db.prepare(
|
|
1997
|
+
`UPDATE agent_memories
|
|
1998
|
+
SET superseded_by = ?,
|
|
1999
|
+
updated_at = ?
|
|
2000
|
+
WHERE id = ?
|
|
2001
|
+
AND pinned = 0
|
|
2002
|
+
AND id != ?`
|
|
2003
|
+
);
|
|
2004
|
+
let changes = 0;
|
|
2005
|
+
const tx = db.transaction((ids) => {
|
|
2006
|
+
for (const id of ids) {
|
|
2007
|
+
const r = stmt.run(supersedingId, now, id, supersedingId);
|
|
2008
|
+
changes += r.changes ?? 0;
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
tx(memoryIds);
|
|
2012
|
+
return changes;
|
|
2013
|
+
}
|
|
2014
|
+
function insertConsolidatedMemory(input) {
|
|
2015
|
+
const db = getDb();
|
|
2016
|
+
const id = newId();
|
|
2017
|
+
const sources = input.sources;
|
|
2018
|
+
if (sources.length === 0) {
|
|
2019
|
+
throw new Error("insertConsolidatedMemory \xB7 sources must be non-empty");
|
|
2020
|
+
}
|
|
2021
|
+
const sourceIds = sources.map((s) => s.id);
|
|
2022
|
+
const conf = sources.reduce((max, s) => Math.max(max, s.confidence), 0);
|
|
2023
|
+
const provenance = sources.reduce((sum, s) => sum + (s.provenanceRooms || 1), 0);
|
|
2024
|
+
const tier = input.forceLong || sources.some((s) => s.tier === "long") ? "long" : "short";
|
|
2025
|
+
const createdAt = sources.reduce((max, s) => Math.max(max, s.createdAt), 0);
|
|
2026
|
+
const now = Date.now();
|
|
2027
|
+
const kind = input.kind ?? sources[0].kind;
|
|
2028
|
+
db.prepare(
|
|
2029
|
+
`INSERT INTO agent_memories
|
|
2030
|
+
(id, agent_id, content, kind, source, source_room, confidence, pinned,
|
|
2031
|
+
created_at, updated_at, tier, usage_count, last_used_at,
|
|
2032
|
+
superseded_by, consolidated_from, provenance_rooms)
|
|
2033
|
+
VALUES (?, ?, ?, ?, 'extracted', NULL, ?, 0,
|
|
2034
|
+
?, ?, ?, 0, NULL,
|
|
2035
|
+
NULL, ?, ?)`
|
|
2036
|
+
).run(
|
|
2037
|
+
id,
|
|
2038
|
+
input.agentId,
|
|
2039
|
+
input.content,
|
|
2040
|
+
kind,
|
|
2041
|
+
conf,
|
|
2042
|
+
createdAt,
|
|
2043
|
+
now,
|
|
2044
|
+
tier,
|
|
2045
|
+
JSON.stringify(sourceIds),
|
|
2046
|
+
provenance
|
|
2047
|
+
);
|
|
2048
|
+
return getMemory(id);
|
|
2049
|
+
}
|
|
2050
|
+
function promoteToLong(memoryIds) {
|
|
2051
|
+
if (!memoryIds.length) return 0;
|
|
2052
|
+
const db = getDb();
|
|
2053
|
+
const now = Date.now();
|
|
2054
|
+
const stmt = db.prepare(
|
|
2055
|
+
`UPDATE agent_memories
|
|
2056
|
+
SET tier = 'long',
|
|
2057
|
+
updated_at = ?
|
|
2058
|
+
WHERE id = ?
|
|
2059
|
+
AND tier = 'short'
|
|
2060
|
+
AND superseded_by IS NULL`
|
|
2061
|
+
);
|
|
2062
|
+
let changes = 0;
|
|
2063
|
+
const tx = db.transaction((ids) => {
|
|
2064
|
+
for (const id of ids) {
|
|
2065
|
+
const r = stmt.run(now, id);
|
|
2066
|
+
changes += r.changes ?? 0;
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
tx(memoryIds);
|
|
2070
|
+
return changes;
|
|
2071
|
+
}
|
|
2072
|
+
function recordDream(input) {
|
|
2073
|
+
const db = getDb();
|
|
2074
|
+
const id = newId();
|
|
2075
|
+
db.prepare(
|
|
2076
|
+
`INSERT INTO agent_dreams
|
|
2077
|
+
(id, agent_id, started_at, finished_at, before_count, after_count,
|
|
2078
|
+
decayed, merged, promoted, superseded, notes)
|
|
2079
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2080
|
+
).run(
|
|
2081
|
+
id,
|
|
2082
|
+
input.agentId,
|
|
2083
|
+
input.startedAt,
|
|
2084
|
+
input.finishedAt,
|
|
2085
|
+
input.beforeCount,
|
|
2086
|
+
input.afterCount,
|
|
2087
|
+
input.decayed,
|
|
2088
|
+
input.merged,
|
|
2089
|
+
input.promoted,
|
|
2090
|
+
input.superseded,
|
|
2091
|
+
input.notes ?? null
|
|
2092
|
+
);
|
|
2093
|
+
return id;
|
|
2094
|
+
}
|
|
1850
2095
|
|
|
1851
2096
|
// src/storage/skills.ts
|
|
1852
2097
|
init_db();
|
|
@@ -3677,6 +3922,330 @@ function formatSearchResults(query, results) {
|
|
|
3677
3922
|
return lines.join("\n");
|
|
3678
3923
|
}
|
|
3679
3924
|
|
|
3925
|
+
// src/ai/prompts/dream-prompts.ts
|
|
3926
|
+
function formatMemoryForPrompt(m) {
|
|
3927
|
+
const flag = m.tier === "long" ? " [stable]" : "";
|
|
3928
|
+
return `${m.id}${flag} (${m.kind}, conf=${m.confidence.toFixed(2)}): ${m.content}`;
|
|
3929
|
+
}
|
|
3930
|
+
function buildClusterPrompt(memories, userName) {
|
|
3931
|
+
const lines = memories.map(formatMemoryForPrompt).join("\n");
|
|
3932
|
+
const system = [
|
|
3933
|
+
`You are processing one agent's accumulated long-term memories about ${userName}.`,
|
|
3934
|
+
`Your job NOW is to find near-duplicates \u2014 memories that, if collapsed into one, would lose no information.`,
|
|
3935
|
+
"",
|
|
3936
|
+
`Output STRICT JSON \xB7 a 2-D array of memory ids forming clusters. Singletons MUST NOT appear (any id you don't list is implicitly its own cluster).`,
|
|
3937
|
+
"",
|
|
3938
|
+
`Examples:`,
|
|
3939
|
+
`Input lines \xB7 two are near-duplicates ("prefers concise" + "dislikes long lists"), one stands alone.`,
|
|
3940
|
+
`Output: [["m1","m2"]]`,
|
|
3941
|
+
"",
|
|
3942
|
+
`Input lines \xB7 all distinct.`,
|
|
3943
|
+
`Output: []`,
|
|
3944
|
+
"",
|
|
3945
|
+
`Hard rules:`,
|
|
3946
|
+
`\xB7 Cluster only when BOTH would lose nothing if collapsed. Same theme but different granularity (e.g. "uses Python" vs "prefers typed Python over dynamic JS") are NOT a cluster.`,
|
|
3947
|
+
`\xB7 Output ONLY a JSON array. No prose, no code fence, no explanation.`,
|
|
3948
|
+
`\xB7 Empty array \`[]\` is a valid + correct answer when nothing duplicates.`
|
|
3949
|
+
].join("\n");
|
|
3950
|
+
const user = [
|
|
3951
|
+
`\u2500\u2500\u2500 ${memories.length} MEMORIES \u2500\u2500\u2500`,
|
|
3952
|
+
lines,
|
|
3953
|
+
"",
|
|
3954
|
+
`\u2500\u2500\u2500 YOUR CLUSTERS (JSON) \u2500\u2500\u2500`
|
|
3955
|
+
].join("\n");
|
|
3956
|
+
return { system, user };
|
|
3957
|
+
}
|
|
3958
|
+
function parseClusterOutput(raw, knownIds) {
|
|
3959
|
+
const stripped = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
3960
|
+
if (!stripped) return [];
|
|
3961
|
+
let parsed;
|
|
3962
|
+
try {
|
|
3963
|
+
parsed = JSON.parse(stripped);
|
|
3964
|
+
} catch {
|
|
3965
|
+
return [];
|
|
3966
|
+
}
|
|
3967
|
+
if (!Array.isArray(parsed)) return [];
|
|
3968
|
+
const out = [];
|
|
3969
|
+
for (const cluster of parsed) {
|
|
3970
|
+
if (!Array.isArray(cluster)) continue;
|
|
3971
|
+
const ids = cluster.filter((x) => typeof x === "string" && knownIds.has(x));
|
|
3972
|
+
const dedup = Array.from(new Set(ids));
|
|
3973
|
+
if (dedup.length >= 2) out.push(dedup);
|
|
3974
|
+
}
|
|
3975
|
+
return out;
|
|
3976
|
+
}
|
|
3977
|
+
function buildMergePrompt(cluster, userName) {
|
|
3978
|
+
const lines = cluster.map(formatMemoryForPrompt).join("\n");
|
|
3979
|
+
const system = [
|
|
3980
|
+
`You are collapsing ${cluster.length} near-duplicate memories about ${userName} into ONE canonical memory.`,
|
|
3981
|
+
"",
|
|
3982
|
+
`Output STRICT JSON \xB7 a single object: {"content": "<sentence in the same first-person assertion style>", "kind": "<one of: fact|observation|preference|goal>"}`,
|
|
3983
|
+
"",
|
|
3984
|
+
`Examples:`,
|
|
3985
|
+
`Input: two memories saying "user prefers concise output" + "user dislikes long lists with bullet padding"`,
|
|
3986
|
+
`Output: {"content": "${userName} prefers concise output, never padded lists", "kind": "preference"}`,
|
|
3987
|
+
"",
|
|
3988
|
+
`Hard rules:`,
|
|
3989
|
+
`\xB7 The merged sentence must preserve every distinct claim across the sources \u2014 pick wording that includes both, don't average them.`,
|
|
3990
|
+
`\xB7 Match the language the source memories were written in (English, Chinese, etc.).`,
|
|
3991
|
+
`\xB7 Output ONLY the JSON object. No prose, no code fence.`,
|
|
3992
|
+
`\xB7 Maximum 200 characters in \`content\`.`
|
|
3993
|
+
].join("\n");
|
|
3994
|
+
const user = [
|
|
3995
|
+
`\u2500\u2500\u2500 CLUSTER (${cluster.length} memories) \u2500\u2500\u2500`,
|
|
3996
|
+
lines,
|
|
3997
|
+
"",
|
|
3998
|
+
`\u2500\u2500\u2500 YOUR MERGED MEMORY (JSON) \u2500\u2500\u2500`
|
|
3999
|
+
].join("\n");
|
|
4000
|
+
return { system, user };
|
|
4001
|
+
}
|
|
4002
|
+
var MERGE_KINDS = /* @__PURE__ */ new Set([
|
|
4003
|
+
"fact",
|
|
4004
|
+
"observation",
|
|
4005
|
+
"preference",
|
|
4006
|
+
"goal"
|
|
4007
|
+
]);
|
|
4008
|
+
function parseMergeOutput(raw) {
|
|
4009
|
+
const stripped = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
4010
|
+
if (!stripped) return null;
|
|
4011
|
+
let parsed;
|
|
4012
|
+
try {
|
|
4013
|
+
parsed = JSON.parse(stripped);
|
|
4014
|
+
} catch {
|
|
4015
|
+
return null;
|
|
4016
|
+
}
|
|
4017
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
4018
|
+
const obj = parsed;
|
|
4019
|
+
const content = typeof obj.content === "string" ? obj.content.trim() : "";
|
|
4020
|
+
if (!content || content.length > 200) return null;
|
|
4021
|
+
const kindRaw = typeof obj.kind === "string" ? obj.kind : "fact";
|
|
4022
|
+
const kind = MERGE_KINDS.has(kindRaw) ? kindRaw : "fact";
|
|
4023
|
+
return { content, kind };
|
|
4024
|
+
}
|
|
4025
|
+
function buildConflictPrompt(memories, userName) {
|
|
4026
|
+
const lines = memories.map((m) => {
|
|
4027
|
+
const d = new Date(m.createdAt).toISOString().slice(0, 10);
|
|
4028
|
+
return `${m.id} (${d}): ${m.content}`;
|
|
4029
|
+
}).join("\n");
|
|
4030
|
+
const system = [
|
|
4031
|
+
`You are looking for direct contradictions among ${userName}'s long-term memories.`,
|
|
4032
|
+
`A "contradiction" is a pair where the newer memory makes a claim that's incompatible with what the older one said \u2014 i.e., ${userName}'s view evolved.`,
|
|
4033
|
+
"",
|
|
4034
|
+
`Output STRICT JSON \xB7 array of {"older": "<id>", "newer": "<id>", "why": "<brief reason>"}.`,
|
|
4035
|
+
"",
|
|
4036
|
+
`Examples:`,
|
|
4037
|
+
`Two memories \xB7 old "user is exploring crypto" + newer "user has decided crypto isn't relevant".`,
|
|
4038
|
+
`Output: [{"older": "m3", "newer": "m9", "why": "exploration \u2192 rejected"}]`,
|
|
4039
|
+
"",
|
|
4040
|
+
`Two memories \xB7 "user is in fintech" + "user prefers concise output". Different topics \u2014 NOT a contradiction.`,
|
|
4041
|
+
`Output: []`,
|
|
4042
|
+
"",
|
|
4043
|
+
`Hard rules:`,
|
|
4044
|
+
`\xB7 Only pair memories that make incompatible claims about the SAME thing. Different topics \u2260 contradiction.`,
|
|
4045
|
+
`\xB7 Newer always wins \u2014 older goes in "older", newer in "newer". Use the date stamps to determine ordering.`,
|
|
4046
|
+
`\xB7 Output ONLY a JSON array. No prose, no code fence.`,
|
|
4047
|
+
`\xB7 Empty array \`[]\` is the correct answer when nothing contradicts.`
|
|
4048
|
+
].join("\n");
|
|
4049
|
+
const user = [
|
|
4050
|
+
`\u2500\u2500\u2500 ${memories.length} MEMORIES (id, date, content) \u2500\u2500\u2500`,
|
|
4051
|
+
lines,
|
|
4052
|
+
"",
|
|
4053
|
+
`\u2500\u2500\u2500 YOUR CONTRADICTIONS (JSON) \u2500\u2500\u2500`
|
|
4054
|
+
].join("\n");
|
|
4055
|
+
return { system, user };
|
|
4056
|
+
}
|
|
4057
|
+
function parseConflictOutput(raw, knownIds) {
|
|
4058
|
+
const stripped = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
4059
|
+
if (!stripped) return [];
|
|
4060
|
+
let parsed;
|
|
4061
|
+
try {
|
|
4062
|
+
parsed = JSON.parse(stripped);
|
|
4063
|
+
} catch {
|
|
4064
|
+
return [];
|
|
4065
|
+
}
|
|
4066
|
+
if (!Array.isArray(parsed)) return [];
|
|
4067
|
+
const out = [];
|
|
4068
|
+
for (const item of parsed) {
|
|
4069
|
+
if (!item || typeof item !== "object") continue;
|
|
4070
|
+
const obj = item;
|
|
4071
|
+
const older = typeof obj.older === "string" ? obj.older : "";
|
|
4072
|
+
const newer = typeof obj.newer === "string" ? obj.newer : "";
|
|
4073
|
+
const why = typeof obj.why === "string" ? obj.why.slice(0, 200) : "";
|
|
4074
|
+
if (!older || !newer) continue;
|
|
4075
|
+
if (older === newer) continue;
|
|
4076
|
+
if (!knownIds.has(older) || !knownIds.has(newer)) continue;
|
|
4077
|
+
out.push({ older, newer, why });
|
|
4078
|
+
}
|
|
4079
|
+
return out;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
// src/orchestrator/dream.ts
|
|
4083
|
+
var DREAM_TRIGGER_THRESHOLD_DIRECTOR = 5;
|
|
4084
|
+
var DREAM_TRIGGER_THRESHOLD_CHAIR = 3;
|
|
4085
|
+
var DREAM_BOOT_FORCE_CEILING_DIRECTOR = 80;
|
|
4086
|
+
var DREAM_BOOT_FORCE_CEILING_CHAIR = 50;
|
|
4087
|
+
function triggerThresholdFor(role) {
|
|
4088
|
+
return role === "moderator" ? DREAM_TRIGGER_THRESHOLD_CHAIR : DREAM_TRIGGER_THRESHOLD_DIRECTOR;
|
|
4089
|
+
}
|
|
4090
|
+
function bootCeilingFor(role) {
|
|
4091
|
+
return role === "moderator" ? DREAM_BOOT_FORCE_CEILING_CHAIR : DREAM_BOOT_FORCE_CEILING_DIRECTOR;
|
|
4092
|
+
}
|
|
4093
|
+
var adjournCounter = /* @__PURE__ */ new Map();
|
|
4094
|
+
function bumpAdjournCounter(agentId, role) {
|
|
4095
|
+
const next = (adjournCounter.get(agentId) ?? 0) + 1;
|
|
4096
|
+
adjournCounter.set(agentId, next);
|
|
4097
|
+
return next >= triggerThresholdFor(role);
|
|
4098
|
+
}
|
|
4099
|
+
function resetAdjournCounter(agentId) {
|
|
4100
|
+
adjournCounter.set(agentId, 0);
|
|
4101
|
+
}
|
|
4102
|
+
var CLUSTER_MIN_SIZE = 6;
|
|
4103
|
+
var CLUSTER_MAX_SIZE = 60;
|
|
4104
|
+
var PROMOTE_MIN_PROVENANCE = 3;
|
|
4105
|
+
var PROMOTE_MIN_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4106
|
+
var PROMOTE_MIN_CONFIDENCE = 0.6;
|
|
4107
|
+
async function runDreamCycle(agentId, config = {}) {
|
|
4108
|
+
const startedAt = Date.now();
|
|
4109
|
+
const beforeCount = countMemoriesForAgent(agentId);
|
|
4110
|
+
const userName = getPrefs().name?.trim() || "the user";
|
|
4111
|
+
const decayed = decayShortTermMemories(agentId, config.decay);
|
|
4112
|
+
let shortPool = listTierForAgent(agentId, "short").filter((m) => !m.pinned);
|
|
4113
|
+
let merged = 0;
|
|
4114
|
+
let supersededCount = 0;
|
|
4115
|
+
let promoted = 0;
|
|
4116
|
+
const utility = config.skipLLM ? null : utilityModelFor();
|
|
4117
|
+
if (utility && shortPool.length >= CLUSTER_MIN_SIZE && shortPool.length <= CLUSTER_MAX_SIZE) {
|
|
4118
|
+
try {
|
|
4119
|
+
const { system, user } = buildClusterPrompt(shortPool, userName);
|
|
4120
|
+
const raw = await callLLM({
|
|
4121
|
+
modelV: utility,
|
|
4122
|
+
messages: [
|
|
4123
|
+
{ role: "system", content: system },
|
|
4124
|
+
{ role: "user", content: user }
|
|
4125
|
+
],
|
|
4126
|
+
temperature: 0.2,
|
|
4127
|
+
maxTokens: 600
|
|
4128
|
+
});
|
|
4129
|
+
const knownIds = new Set(shortPool.map((m) => m.id));
|
|
4130
|
+
const clusters = parseClusterOutput(raw, knownIds);
|
|
4131
|
+
const byId = new Map(shortPool.map((m) => [m.id, m]));
|
|
4132
|
+
for (const ids of clusters) {
|
|
4133
|
+
const sources = ids.map((id) => byId.get(id)).filter((m) => !!m);
|
|
4134
|
+
if (sources.length < 2) continue;
|
|
4135
|
+
try {
|
|
4136
|
+
const mergePrompt = buildMergePrompt(sources, userName);
|
|
4137
|
+
const mergeRaw = await callLLM({
|
|
4138
|
+
modelV: utility,
|
|
4139
|
+
messages: [
|
|
4140
|
+
{ role: "system", content: mergePrompt.system },
|
|
4141
|
+
{ role: "user", content: mergePrompt.user }
|
|
4142
|
+
],
|
|
4143
|
+
temperature: 0.2,
|
|
4144
|
+
maxTokens: 200
|
|
4145
|
+
});
|
|
4146
|
+
const result = parseMergeOutput(mergeRaw);
|
|
4147
|
+
if (!result) continue;
|
|
4148
|
+
const consolidated = insertConsolidatedMemory({
|
|
4149
|
+
agentId,
|
|
4150
|
+
content: result.content,
|
|
4151
|
+
kind: result.kind,
|
|
4152
|
+
sources
|
|
4153
|
+
});
|
|
4154
|
+
const supersededByMerge = markSuperseded(
|
|
4155
|
+
sources.map((s) => s.id),
|
|
4156
|
+
consolidated.id
|
|
4157
|
+
);
|
|
4158
|
+
merged += 1;
|
|
4159
|
+
supersededCount += supersededByMerge;
|
|
4160
|
+
} catch (e) {
|
|
4161
|
+
process.stderr.write(
|
|
4162
|
+
`[dream] merge step for one cluster failed: ${e instanceof Error ? e.message : String(e)}
|
|
4163
|
+
`
|
|
4164
|
+
);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
} catch (e) {
|
|
4168
|
+
process.stderr.write(
|
|
4169
|
+
`[dream] cluster step failed: ${e instanceof Error ? e.message : String(e)}
|
|
4170
|
+
`
|
|
4171
|
+
);
|
|
4172
|
+
}
|
|
4173
|
+
shortPool = listTierForAgent(agentId, "short").filter((m) => !m.pinned);
|
|
4174
|
+
}
|
|
4175
|
+
if (utility && shortPool.length >= 2 && shortPool.length <= CLUSTER_MAX_SIZE) {
|
|
4176
|
+
try {
|
|
4177
|
+
const { system, user } = buildConflictPrompt(shortPool, userName);
|
|
4178
|
+
const raw = await callLLM({
|
|
4179
|
+
modelV: utility,
|
|
4180
|
+
messages: [
|
|
4181
|
+
{ role: "system", content: system },
|
|
4182
|
+
{ role: "user", content: user }
|
|
4183
|
+
],
|
|
4184
|
+
temperature: 0.2,
|
|
4185
|
+
maxTokens: 400
|
|
4186
|
+
});
|
|
4187
|
+
const knownIds = new Set(shortPool.map((m) => m.id));
|
|
4188
|
+
const pairs = parseConflictOutput(raw, knownIds);
|
|
4189
|
+
for (const pair of pairs) {
|
|
4190
|
+
const n = markSuperseded([pair.older], pair.newer);
|
|
4191
|
+
supersededCount += n;
|
|
4192
|
+
}
|
|
4193
|
+
} catch (e) {
|
|
4194
|
+
process.stderr.write(
|
|
4195
|
+
`[dream] conflict step failed: ${e instanceof Error ? e.message : String(e)}
|
|
4196
|
+
`
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
const fresh = listTierForAgent(agentId, "short").filter((m) => !m.pinned);
|
|
4201
|
+
const ageCutoff = Date.now() - PROMOTE_MIN_AGE_MS;
|
|
4202
|
+
const promoteIds = fresh.filter(
|
|
4203
|
+
(m) => m.provenanceRooms >= PROMOTE_MIN_PROVENANCE && m.createdAt <= ageCutoff && m.confidence >= PROMOTE_MIN_CONFIDENCE
|
|
4204
|
+
).map((m) => m.id);
|
|
4205
|
+
if (promoteIds.length > 0) {
|
|
4206
|
+
promoted = promoteToLong(promoteIds);
|
|
4207
|
+
}
|
|
4208
|
+
const finishedAt = Date.now();
|
|
4209
|
+
const afterCount = countMemoriesForAgent(agentId);
|
|
4210
|
+
const agent = getAgent(agentId);
|
|
4211
|
+
const label = agent ? `${agent.name} (${agentId.slice(0, 8)})` : agentId.slice(0, 8);
|
|
4212
|
+
process.stderr.write(
|
|
4213
|
+
`[dream] ${label} \xB7 before=${beforeCount} after=${afterCount} decayed=${decayed} merged=${merged} promoted=${promoted} superseded=${supersededCount} took=${finishedAt - startedAt}ms
|
|
4214
|
+
`
|
|
4215
|
+
);
|
|
4216
|
+
try {
|
|
4217
|
+
recordDream({
|
|
4218
|
+
agentId,
|
|
4219
|
+
startedAt,
|
|
4220
|
+
finishedAt,
|
|
4221
|
+
beforeCount,
|
|
4222
|
+
afterCount,
|
|
4223
|
+
decayed,
|
|
4224
|
+
merged,
|
|
4225
|
+
promoted,
|
|
4226
|
+
superseded: supersededCount,
|
|
4227
|
+
notes: utility ? `utility=${utility}` : "no-utility-model \xB7 LLM steps skipped"
|
|
4228
|
+
});
|
|
4229
|
+
} catch (e) {
|
|
4230
|
+
process.stderr.write(
|
|
4231
|
+
`[dream] audit log failed: ${e instanceof Error ? e.message : String(e)}
|
|
4232
|
+
`
|
|
4233
|
+
);
|
|
4234
|
+
}
|
|
4235
|
+
resetAdjournCounter(agentId);
|
|
4236
|
+
return {
|
|
4237
|
+
agentId,
|
|
4238
|
+
startedAt,
|
|
4239
|
+
finishedAt,
|
|
4240
|
+
beforeCount,
|
|
4241
|
+
afterCount,
|
|
4242
|
+
decayed,
|
|
4243
|
+
merged,
|
|
4244
|
+
promoted,
|
|
4245
|
+
superseded: supersededCount
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
|
|
3680
4249
|
// src/routes/agents.ts
|
|
3681
4250
|
function agentSpecModelCandidates() {
|
|
3682
4251
|
const out = [];
|
|
@@ -4025,6 +4594,9 @@ function agentsRouter() {
|
|
|
4025
4594
|
if (typeof b.webSearchEnabled === "boolean") {
|
|
4026
4595
|
patch.webSearchEnabled = b.webSearchEnabled;
|
|
4027
4596
|
}
|
|
4597
|
+
if (typeof b.isPinned === "boolean") {
|
|
4598
|
+
patch.isPinned = b.isPinned;
|
|
4599
|
+
}
|
|
4028
4600
|
const updated = updateAgent(id, patch);
|
|
4029
4601
|
return c.json(updated);
|
|
4030
4602
|
});
|
|
@@ -4115,6 +4687,24 @@ function agentsRouter() {
|
|
|
4115
4687
|
if (!ok) return c.json({ error: "not found" }, 404);
|
|
4116
4688
|
return c.json({ ok: true });
|
|
4117
4689
|
});
|
|
4690
|
+
r.post("/:id/dream", async (c) => {
|
|
4691
|
+
const id = c.req.param("id");
|
|
4692
|
+
if (!getAgent(id)) return c.json({ error: "not found" }, 404);
|
|
4693
|
+
let body = {};
|
|
4694
|
+
try {
|
|
4695
|
+
body = await c.req.json();
|
|
4696
|
+
} catch {
|
|
4697
|
+
}
|
|
4698
|
+
const aggressive = !!(body && typeof body === "object" && body.aggressive);
|
|
4699
|
+
try {
|
|
4700
|
+
const summary = await runDreamCycle(id, aggressive ? { decay: { minAgeMs: 15 * 24 * 60 * 60 * 1e3, maxConfidence: 0.7 } } : void 0);
|
|
4701
|
+
resetAdjournCounter(id);
|
|
4702
|
+
return c.json({ ok: true, summary });
|
|
4703
|
+
} catch (e) {
|
|
4704
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4705
|
+
return c.json({ error: msg }, 500);
|
|
4706
|
+
}
|
|
4707
|
+
});
|
|
4118
4708
|
r.get("/:id/skills", (c) => {
|
|
4119
4709
|
const id = c.req.param("id");
|
|
4120
4710
|
const agent = getAgent(id);
|
|
@@ -5349,25 +5939,473 @@ function pickedBlock(picked) {
|
|
|
5349
5939
|
for (const k of allKnown) if (!set.has(k)) skipped.push(k);
|
|
5350
5940
|
return [
|
|
5351
5941
|
``,
|
|
5352
|
-
`\u2500\u2500\u2500 COMPOSER PICKED COMPONENTS \u2500\u2500\u2500`,
|
|
5942
|
+
`\u2500\u2500\u2500 COMPOSER PICKED COMPONENTS \u2500\u2500\u2500`,
|
|
5943
|
+
``,
|
|
5944
|
+
`The composer (Stage 1.5) picked these components for this brief \u2014 fill ONLY these fields:`,
|
|
5945
|
+
...[...set].sort().map((k) => ` \xB7 ${k}`),
|
|
5946
|
+
``,
|
|
5947
|
+
`Skip these components (set their fields to empty/null per the substitute-group rules in the system prompt):`,
|
|
5948
|
+
...skipped.sort().map((k) => ` \xB7 ${k}`),
|
|
5949
|
+
``,
|
|
5950
|
+
`\u2500\u2500\u2500 END PICKED \u2500\u2500\u2500`
|
|
5951
|
+
].join("\n");
|
|
5952
|
+
}
|
|
5953
|
+
function buildScaffoldMessages(opts) {
|
|
5954
|
+
const { room, members, perDirectorSignals, language, picked } = opts;
|
|
5955
|
+
const memberList = members.map((a) => `${a.id} \xB7 ${a.name} (${a.handle}) \u2014 ${a.roleTag}`).join("\n \xB7 ");
|
|
5956
|
+
const signalsBlock = perDirectorSignals.map((d) => {
|
|
5957
|
+
if (!d.signals.length) return `[${d.directorId}] ${d.directorName} \u2014 (no signals)`;
|
|
5958
|
+
const lines = d.signals.map(
|
|
5959
|
+
(s, i) => ` \xB7 ${d.directorId}#${i} [${s.lens}] ${s.text}`
|
|
5960
|
+
).join("\n");
|
|
5961
|
+
return `[${d.directorId}] ${d.directorName}
|
|
5962
|
+
${lines}`;
|
|
5963
|
+
}).join("\n\n");
|
|
5964
|
+
const supplementBlock = opts.supplement && opts.supplement.trim() ? [
|
|
5965
|
+
``,
|
|
5966
|
+
`\u2500\u2500\u2500 SUPPLEMENTARY PERSPECTIVE FROM USER \u2500\u2500\u2500`,
|
|
5967
|
+
``,
|
|
5968
|
+
`The user has asked you to additionally consider this angle when building the scaffold. Address it explicitly \u2014 work it into the scaffold's findings, divergence, recommendations, and/or new questions wherever it lands most cleanly. Do NOT add a separate section for it; weave it through.`,
|
|
5969
|
+
``,
|
|
5970
|
+
opts.supplement.trim(),
|
|
5971
|
+
``,
|
|
5972
|
+
`\u2500\u2500\u2500 END SUPPLEMENT \u2500\u2500\u2500`
|
|
5973
|
+
].join("\n") : "";
|
|
5974
|
+
return [
|
|
5975
|
+
{
|
|
5976
|
+
role: "system",
|
|
5977
|
+
content: [SCAFFOLD_SYSTEM, "", languageInstruction(language)].join("\n")
|
|
5978
|
+
},
|
|
5979
|
+
{
|
|
5980
|
+
role: "user",
|
|
5981
|
+
content: [
|
|
5982
|
+
`ROOM #${room.number} \xB7 ${room.name}`,
|
|
5983
|
+
`Subject: ${room.subject}`,
|
|
5984
|
+
// `Mode: …` deliberately omitted · see composer.ts for the
|
|
5985
|
+
// same change. Surfacing the room mode here biased the LLM
|
|
5986
|
+
// toward critique-shaped / brainstorm-shaped output even
|
|
5987
|
+
// though the standard scaffold prompt asks for decision-grade
|
|
5988
|
+
// JSON, leading to parseScaffold rejecting every retry.
|
|
5989
|
+
``,
|
|
5990
|
+
`Directors:`,
|
|
5991
|
+
` \xB7 ${memberList}`,
|
|
5992
|
+
``,
|
|
5993
|
+
`\u2500\u2500\u2500 SIGNALS \u2500\u2500\u2500`,
|
|
5994
|
+
``,
|
|
5995
|
+
signalsBlock || "(no signals extracted)",
|
|
5996
|
+
``,
|
|
5997
|
+
`\u2500\u2500\u2500 END SIGNALS \u2500\u2500\u2500`,
|
|
5998
|
+
pickedBlock(picked),
|
|
5999
|
+
supplementBlock,
|
|
6000
|
+
``,
|
|
6001
|
+
`Produce the scaffold now. JSON only.`
|
|
6002
|
+
].join("\n")
|
|
6003
|
+
}
|
|
6004
|
+
];
|
|
6005
|
+
}
|
|
6006
|
+
var MAGAZINE_SYSTEM = [
|
|
6007
|
+
'You are the chair of a boardroom session. You produce a MAGAZINE-SPREAD report \u2014 an editorial single-page layout that opens like a magazine cover, lays out a numbered card grid of takeaways, walks through a 3-step setup band, and closes with a high-contrast "why this matters" pull-list. Not a memo. Not a research note. A magazine.',
|
|
6008
|
+
"",
|
|
6009
|
+
"## What a magazine spread is for",
|
|
6010
|
+
"",
|
|
6011
|
+
"Magazine is for the moment when the user wants to share the takeaway as a *piece of editorial content* \u2014 something a reader could scroll on a feed. Not the analysis itself \xB7 the cover line + the numbered tactics + the setup recipe. Not the room's debate \xB7 the published version a stranger could understand cold.",
|
|
6012
|
+
"",
|
|
6013
|
+
'Editorial register \xB7 magazines lead with personality, not with hedging. Headlines are claim-style, decks have voice, talking points read like "5 tactics that actually work" rather than "considerations". You are writing the cover spread, not the white paper.',
|
|
6014
|
+
"",
|
|
6015
|
+
"## The 8 slots you fill (output is JSON only)",
|
|
6016
|
+
"",
|
|
6017
|
+
'1. **title** \xB7 the cover headline \xB7 serif display \xB7 \u2264 110 chars \xB7 ONE sentence in claim form. Magazine covers state the takeaway with confidence \u2014 quantified or named ("How {chair} {verb} {object}" / "X is the operating system for Y" / "Three commitments that change the trajectory").',
|
|
6018
|
+
"",
|
|
6019
|
+
"2. **kicker** \xB7 the cover deck \xB7 \u2264 200 chars \xB7 1 sentence under the headline. Non-italic register \xB7 subtitle voice. Names the angle / what's new.",
|
|
6020
|
+
"",
|
|
6021
|
+
'3. **source** \xB7 \u2264 80 chars \xB7 the masthead byline \xB7 "From {chair name} \xB7 {date}" / "Issue 01 \xB7 {date}" / "{chair name} presents". Mono small caps register; auto-filled if you skip.',
|
|
6022
|
+
"",
|
|
6023
|
+
`4. **milestones** \xB7 EXACTLY 3 cards \xB7 these become the magazine's middle band \u2014 a "how to set this up in 10 minutes" 3-step recipe. Each card has:`,
|
|
6024
|
+
' \xB7 `period` \xB7 short step label \xB7 \u2264 24 chars \xB7 "Step 1" / "\u51C6\u5907" / "Phase 2" / "First". Imperative or sequential.',
|
|
6025
|
+
' \xB7 `title` \xB7 \u2264 60 chars \xB7 what to do at this step. Imperative voice ("Set up environment" / "\u51C6\u5907\u73AF\u5883").',
|
|
6026
|
+
" \xB7 `body` \xB7 2 sentences \xB7 \u2264 220 chars \xB7 the concrete instruction \xB7 how a reader actually does this step.",
|
|
6027
|
+
" \xB7 `callout` \xB7 \u2264 12 chars \xB7 optional anchor numeric \xB7 usually empty in magazine mode (the layout doesn't lean on big numbers in this band).",
|
|
6028
|
+
" \xB7 `tags` \xB7 empty array (`[]`) \xB7 the magazine layout doesn't render tags on the setup band.",
|
|
6029
|
+
"",
|
|
6030
|
+
"5. **rankedBars** \xB7 OPTIONAL \xB7 3-5 ranked entries. Renders only when the room produced a clean ranking. Set to `null` when there's no real ranked-numeric material.",
|
|
6031
|
+
"",
|
|
6032
|
+
`6. **verification** \xB7 MANDATORY for magazine \xB7 these become the dark closing band's "why this matters" pull-list. Provide 4 bullets \xB7 \u2264 140 chars each \xB7 each bullet is a SHORT REASON the takeaway matters \xB7 phrased as a stand-alone declaration ("Saves time \xB7 routine work compresses to minutes" / "Highly personalized \xB7 tailored to your context"). Title is your call (default "Why this matters" / "\u4E3A\u4EC0\u4E48\u8FD9\u5F88\u5F3A\u5927").`,
|
|
6033
|
+
"",
|
|
6034
|
+
`7. **talkingPoints** \xB7 MANDATORY \xB7 5 numbered cards \xB7 \u2264 120 chars each \xB7 these become the magazine's hero card grid \xB7 the "5 tactics" feature. EACH BULLET MUST BEGIN WITH A SHORT PHRASE (the card's title \u2014 what the renderer extracts as the card headline) FOLLOWED BY " \xB7 " (a middle-dot with spaces) AND THEN THE BODY SENTENCE. Example format: "Weekly check-in \xB7 Run /weekly check-in to track key metrics across a personal dashboard." The renderer splits on " \xB7 " to extract title + body. If you can't fit 5, output as many as you have \u22653.`,
|
|
6035
|
+
"",
|
|
6036
|
+
' Title side \u2264 24 chars \xB7 body side \u2264 100 chars. Imperative voice in the body ("Run X" / "\u4F7F\u7528 X"). Imperative for English magazine voice; in Chinese, lead the body with the verb ("\u4F7F\u7528\u2026" / "\u8FD0\u884C\u2026" / "\u901A\u8FC7\u2026").',
|
|
6037
|
+
"",
|
|
6038
|
+
` Default talkingPoints title \xB7 "5 tactics" / "5 \u4E2A\u4F8B\u5B50\u770B\u660E\u767D" / "5 ways to use this" \xB7 the LARGE outline numeral on the magazine cover is derived from this section's count.`,
|
|
6039
|
+
"",
|
|
6040
|
+
"8. **conclusion** \xB7 \u2264 100 chars \xB7 ONE sentence \xB7 the cover-line reinforcement. The reader walks away with this single line \xB7 usually a short imperative or claim restatement.",
|
|
6041
|
+
"",
|
|
6042
|
+
"Plus optional **flow** \xB7 usually `null` in magazine mode \xB7 only fill when the room argued a clean transformation arc.",
|
|
6043
|
+
"",
|
|
6044
|
+
"Plus auto **footerTag** \xB7 \u2264 80 chars \xB7 masthead-style caption \xB7 auto-filled if you skip.",
|
|
6045
|
+
"",
|
|
6046
|
+
"## Routing the SIGNALS block into magazine slots",
|
|
6047
|
+
"",
|
|
6048
|
+
" \xB7 **title** \u2190 the strongest claim \xB7 phrased as a magazine cover headline.",
|
|
6049
|
+
" \xB7 **kicker** \u2190 the supporting deck \xB7 \u2264 1 sentence \xB7 what's new about this conclusion.",
|
|
6050
|
+
" \xB7 **milestones** \u2190 if the room argued a setup / how-to flow, route the 3 most important steps. If the room didn't produce a clean recipe, distill the 3 most actionable items into imperative steps.",
|
|
6051
|
+
' \xB7 **talkingPoints** \u2190 the 5 strongest action / claim entries \xB7 each rewritten in "Title \xB7 Body" form (split with the middle dot + spaces).',
|
|
6052
|
+
" \xB7 **verification** \u2190 4 reasons the takeaway matters \xB7 drawn from the room's evidence + bottom-line material \xB7 phrased as standalone declarations.",
|
|
6053
|
+
" \xB7 **conclusion** \u2190 compressed restatement of the room's anchor.",
|
|
6054
|
+
"",
|
|
6055
|
+
"## Output format",
|
|
6056
|
+
"",
|
|
6057
|
+
"Strict JSON inside a fenced ```json code block. No prose outside the block. The shape is fixed:",
|
|
6058
|
+
"",
|
|
6059
|
+
"```json",
|
|
6060
|
+
"{",
|
|
6061
|
+
' "title": "Cover-line takeaway in claim form.",',
|
|
6062
|
+
' "kicker": "1-sentence deck explaining the angle.",',
|
|
6063
|
+
' "source": "From {chair name} \xB7 {date}",',
|
|
6064
|
+
' "milestones": [',
|
|
6065
|
+
' { "period": "Step 1", "title": "Set up environment", "body": "Install the tooling and create the workspace folder.", "callout": "", "tags": [] },',
|
|
6066
|
+
' { "period": "Step 2", "title": "Initialize and configure", "body": "Run the init command and follow the prompts to wire up your slash commands.", "callout": "", "tags": [] },',
|
|
6067
|
+
' { "period": "Step 3", "title": "Run and customize", "body": "Run weekly or daily, customize prompts to your context.", "callout": "", "tags": [] }',
|
|
6068
|
+
" ],",
|
|
6069
|
+
' "rankedBars": null,',
|
|
6070
|
+
' "verification": {',
|
|
6071
|
+
' "title": "Why this matters",',
|
|
6072
|
+
' "bullets": [',
|
|
6073
|
+
' "Saves time \xB7 routine work compresses to minutes a week.",',
|
|
6074
|
+
' "Highly personalized \xB7 tailored to the context you provide.",',
|
|
6075
|
+
' "Beyond coding \xB7 use cases extend well past development tasks.",',
|
|
6076
|
+
' "Infinite extensibility \xB7 spin up more agents to fit any new need."',
|
|
6077
|
+
" ]",
|
|
6078
|
+
" },",
|
|
6079
|
+
' "talkingPoints": {',
|
|
6080
|
+
' "title": "5 tactics",',
|
|
6081
|
+
' "bullets": [',
|
|
6082
|
+
' "Weekly check-in \xB7 Run /weekly check-in to track key metrics on a personal dashboard.",',
|
|
6083
|
+
' "Daily journal \xB7 Run /daily check-in to journal accomplishments and feelings.",',
|
|
6084
|
+
' "Content research \xB7 Use /newsletter researcher to draft your own briefs in your voice.",',
|
|
6085
|
+
' "Brain-dump analyzer \xB7 Run /brain dump analysis on raw notes to surface a mind-map.",',
|
|
6086
|
+
' "Daily brief \xB7 Use /daily brief for a tailored news round-up by your interests."',
|
|
6087
|
+
" ]",
|
|
6088
|
+
" },",
|
|
6089
|
+
' "conclusion": "One-line takeaway \xB7 \u2264 100 chars.",',
|
|
6090
|
+
' "flow": null,',
|
|
6091
|
+
' "footerTag": "Issue 01 \xB7 {date}"',
|
|
6092
|
+
"}",
|
|
6093
|
+
"```",
|
|
6094
|
+
"",
|
|
6095
|
+
"Constraints:",
|
|
6096
|
+
`\xB7 Title MUST be cover-style (a magazine wouldn't run "Analysis of strategic options" \u2014 it would run "How X built the operating system for Y"). State the takeaway, not the topic.`,
|
|
6097
|
+
`\xB7 talkingPoints bullets MUST follow "Title \xB7 Body" with the middle dot + spaces \xB7 the renderer's split is exact.`,
|
|
6098
|
+
"\xB7 Provide 4 verification bullets when the room has the material; 3 acceptable as a floor.",
|
|
6099
|
+
"\xB7 No markdown formatting inside string fields. No bullet characters. No headings. Plain prose only \u2014 the renderer adds visual structure."
|
|
6100
|
+
].join("\n");
|
|
6101
|
+
function buildMagazineMessages(opts) {
|
|
6102
|
+
const { room, members, perDirectorSignals, language } = opts;
|
|
6103
|
+
const memberList = members.map((a) => `${a.id} \xB7 ${a.name} (${a.handle}) \u2014 ${a.roleTag}`).join("\n \xB7 ");
|
|
6104
|
+
const signalsBlock = perDirectorSignals.map((d) => {
|
|
6105
|
+
if (!d.signals.length) return `[${d.directorId}] ${d.directorName} \u2014 (no signals)`;
|
|
6106
|
+
const lines = d.signals.map((s, i) => ` \xB7 ${d.directorId}#${i} [${s.lens}] ${s.text}`).join("\n");
|
|
6107
|
+
return `[${d.directorId}] ${d.directorName}
|
|
6108
|
+
${lines}`;
|
|
6109
|
+
}).join("\n\n");
|
|
6110
|
+
const supplementBlock = opts.supplement && opts.supplement.trim() ? [
|
|
6111
|
+
``,
|
|
6112
|
+
`\u2500\u2500\u2500 SUPPLEMENTARY PERSPECTIVE FROM USER \u2500\u2500\u2500`,
|
|
6113
|
+
``,
|
|
6114
|
+
`The user has asked you to additionally consider this angle when building the magazine. Surface it in the most fitting slot (most often as one of the talking points or verification bullets).`,
|
|
6115
|
+
``,
|
|
6116
|
+
opts.supplement.trim(),
|
|
6117
|
+
``,
|
|
6118
|
+
`\u2500\u2500\u2500 END SUPPLEMENT \u2500\u2500\u2500`
|
|
6119
|
+
].join("\n") : "";
|
|
6120
|
+
return [
|
|
6121
|
+
{
|
|
6122
|
+
role: "system",
|
|
6123
|
+
content: [MAGAZINE_SYSTEM, "", languageInstruction(language)].join("\n")
|
|
6124
|
+
},
|
|
6125
|
+
{
|
|
6126
|
+
role: "user",
|
|
6127
|
+
content: [
|
|
6128
|
+
`ROOM #${room.number} \xB7 ${room.name}`,
|
|
6129
|
+
`Subject: ${room.subject}`,
|
|
6130
|
+
``,
|
|
6131
|
+
`Directors:`,
|
|
6132
|
+
` \xB7 ${memberList}`,
|
|
6133
|
+
``,
|
|
6134
|
+
`\u2500\u2500\u2500 SIGNALS \u2500\u2500\u2500`,
|
|
6135
|
+
``,
|
|
6136
|
+
signalsBlock || "(no signals extracted)",
|
|
6137
|
+
``,
|
|
6138
|
+
`\u2500\u2500\u2500 END SIGNALS \u2500\u2500\u2500`,
|
|
6139
|
+
supplementBlock,
|
|
6140
|
+
``,
|
|
6141
|
+
`Produce the magazine spread now. JSON only.`
|
|
6142
|
+
].join("\n")
|
|
6143
|
+
}
|
|
6144
|
+
];
|
|
6145
|
+
}
|
|
6146
|
+
var NEWSPAPER_SYSTEM = [
|
|
6147
|
+
"You are the chair of a boardroom session. You produce a MULTI-PAGE NEWSPAPER report \u2014 a broadsheet that spreads across 3 distinct pages (front cover \xB7 inside spread \xB7 back / Best Of). The reader scrolls through them like flipping a real paper. Not a memo. Not a magazine. A NEWSPAPER.",
|
|
6148
|
+
"",
|
|
6149
|
+
"## Voice",
|
|
6150
|
+
"",
|
|
6151
|
+
'Front-page journalism. Headlines are declarative, present-tense, claim-form ("BOARD COMMITS TO TWO-TRACK RELEASE" \u2014 not "An analysis of release strategy" or "What the board decided"). Body prose is lead-paragraph editorial: each paragraph carries one claim with its supporting evidence, sentences are short, transitions are crisp, hedging is minimal.',
|
|
6152
|
+
"",
|
|
6153
|
+
"Newspaper voice is more formal than magazine voice and more confident than research-note voice. Imagine the front page of a serious broadsheet \xB7 The Wall Street Journal, The Financial Times, The Economist if it ran a daily.",
|
|
6154
|
+
"",
|
|
6155
|
+
"## The 8 slots you fill (output is JSON only)",
|
|
6156
|
+
"",
|
|
6157
|
+
'1. **title** \xB7 the front-page banner headline \xB7 \u2264 110 chars \xB7 ALL-CAPS-ABLE claim (the renderer applies uppercase) \xB7 NOT a question, NOT a label, a CLAIM. Examples: "BOARD COMMITS TO TWO-TRACK RELEASE", "MARKETS BRACE FOR Q4 RESET", "REGULATORS MOVE AGAINST DARK PATTERNS".',
|
|
6158
|
+
"",
|
|
6159
|
+
"2. **kicker** \xB7 the subheading deck \xB7 \u2264 200 chars \xB7 1 sentence under the headline \xB7 expands the claim with the angle / what's new.",
|
|
6160
|
+
"",
|
|
6161
|
+
'3. **source** \xB7 masthead byline \xB7 \u2264 80 chars \xB7 "From the desk of {chair name} \xB7 {date}" or similar. Mono small caps register; auto-filled if you skip.',
|
|
6162
|
+
"",
|
|
6163
|
+
"4. **milestones** \xB7 EXACTLY 3 column-stories \xB7 they distribute across the 3 pages. Each card has:",
|
|
6164
|
+
' \xB7 `period` \xB7 column section label \xB7 \u2264 24 chars \xB7 "TOP STORY" / "MARKETS" / "POLICY" / "OPS" / "OPINION". Section-banner register. ALL-CAPS-ABLE.',
|
|
6165
|
+
" \xB7 `title` \xB7 column subheading \xB7 \u2264 60 chars \xB7 the column's hook. Question or claim form OK.",
|
|
6166
|
+
" \xB7 `body` \xB7 4-7 sentences \xB7 \u2264 480 chars \xB7 LONGER than other modes since this fills an editorial column. Lead-paragraph style: open with the claim, support with evidence, close with the so-what. Present tense, declarative.",
|
|
6167
|
+
' \xB7 `callout` \xB7 short numeric / metric \xB7 \u2264 12 chars \xB7 used as the front-page sidebar\'s big number when present ("124,000" / "-10\xD7" / "$120M"). Empty when the milestone has no clean numeric anchor.',
|
|
6168
|
+
" \xB7 `tags` \xB7 empty array.",
|
|
6169
|
+
"",
|
|
6170
|
+
" Page distribution \xB7 ms[0] = the LEAD STORY on page 1's front (drop-cap body) \xB7 ms[1] = the ECONOMY band on page 1 (chart-paired short article) AND continued on page 2 \xB7 ms[2] = the BREAKING NEWS sidebar on page 1 (short headline + body + numeric callout) AND extends on page 2.",
|
|
6171
|
+
"",
|
|
6172
|
+
"5. **rankedBars** \xB7 OPTIONAL \xB7 ECONOMY-band chart on page 1 \xB7 3-5 ranked entries painted as a teal-bar chart. Set null when the room has no clean ranked-numeric material.",
|
|
6173
|
+
"",
|
|
6174
|
+
'6. **verification** \xB7 MANDATORY \xB7 stacked "MORE HEADINGS" sidebar on page 2 \xB7 3-5 entries \xB7 each \u2264 180 chars \xB7 phrased as "Heading: body sentence." \u2014 use a colon as separator (the renderer splits on it for typography). Each entry is a SHORT NEWS ITEM that supports or qualifies the front-page claim.',
|
|
6175
|
+
"",
|
|
6176
|
+
"7. **talkingPoints** \xB7 MANDATORY \xB7 3-6 entries \xB7 these distribute across the document: talking[0] becomes the front-page portrait pull-quote AND the page-2 inset quote; the full list reappears on page 3 as a 2-col grid of feature articles (Best of the Best). Each entry: \u2264 160 chars \xB7 self-contained quotable line \xB7 imperative or declarative.",
|
|
6177
|
+
"",
|
|
6178
|
+
"8. **conclusion** \xB7 the front-page inverted PULL-QUOTE band \xB7 \u2264 100 chars \xB7 ONE sentence \xB7 the takeaway compressed to a quote. The reader walks away with this single line.",
|
|
6179
|
+
"",
|
|
6180
|
+
"Plus optional **flow** \xB7 2-4 short nodes \xB7 transformation arc \xB7 renders as a strip on page 3 when present.",
|
|
6181
|
+
"",
|
|
6182
|
+
"Plus auto **footerTag** \xB7 \u2264 80 chars \xB7 masthead-style date caption \xB7 auto-filled if you skip.",
|
|
6183
|
+
"",
|
|
6184
|
+
"## Routing the SIGNALS block into newspaper slots",
|
|
6185
|
+
"",
|
|
6186
|
+
" \xB7 **title** \u2190 the strongest claim \xB7 phrased as a front-page banner headline (declarative, claim-form).",
|
|
6187
|
+
" \xB7 **kicker** \u2190 the supporting deck \xB7 \u2264 1 sentence \xB7 what's new about this conclusion.",
|
|
6188
|
+
" \xB7 **milestones** \u2190 the 3 most load-bearing pieces \xB7 ms[0] is the lead story (longest body), ms[1] feeds the economy band on page 1, ms[2] feeds the breaking-news sidebar.",
|
|
6189
|
+
` \xB7 **verification** \u2190 the room's secondary findings \xB7 each phrased as "Heading: body." with a colon separator.`,
|
|
6190
|
+
' \xB7 **talkingPoints** \u2190 3-6 quotable lines \xB7 the page-3 "Best of the Best" features.',
|
|
6191
|
+
" \xB7 **conclusion** \u2190 the front-page pull-quote band \xB7 the room's bottom line in claim form.",
|
|
6192
|
+
"",
|
|
6193
|
+
"## Output format",
|
|
6194
|
+
"",
|
|
6195
|
+
"Strict JSON inside a fenced ```json code block. No prose outside the block. The shape is fixed:",
|
|
6196
|
+
"",
|
|
6197
|
+
"```json",
|
|
6198
|
+
"{",
|
|
6199
|
+
' "title": "Banner headline in claim form",',
|
|
6200
|
+
' "kicker": "1-sentence subdeck explaining the angle.",',
|
|
6201
|
+
' "source": "From the desk of {chair name} \xB7 {date}",',
|
|
6202
|
+
' "milestones": [',
|
|
6203
|
+
' { "period": "TOP STORY", "title": "What the board decided", "body": "4-7 sentence editorial column body explaining the lead story with evidence and so-what.", "callout": "", "tags": [] },',
|
|
6204
|
+
' { "period": "MARKETS", "title": "Why the market is reading this", "body": "4-7 sentence editorial column body covering the second angle.", "callout": "", "tags": [] },',
|
|
6205
|
+
' { "period": "POLICY", "title": "Open questions for the regulator", "body": "4-7 sentence editorial column body covering the third angle.", "callout": "", "tags": [] }',
|
|
6206
|
+
" ],",
|
|
6207
|
+
' "rankedBars": null,',
|
|
6208
|
+
' "verification": {',
|
|
6209
|
+
' "title": "More headings",',
|
|
6210
|
+
' "bullets": [',
|
|
6211
|
+
` "Q4 outlook: Three commitments anchor the next quarter's plan.",`,
|
|
6212
|
+
' "Pricing: The pilot programme survives, but caps are tightening.",',
|
|
6213
|
+
' "Hiring: Two new senior roles open by next board meeting.",',
|
|
6214
|
+
' "Risk: Compliance review remains the gating constraint."',
|
|
6215
|
+
" ]",
|
|
6216
|
+
" },",
|
|
6217
|
+
' "talkingPoints": {',
|
|
6218
|
+
' "title": "From the editorial",',
|
|
6219
|
+
' "bullets": [',
|
|
6220
|
+
' "First quotable editorial line that names the takeaway.",',
|
|
6221
|
+
' "Second quotable line that addresses the obvious objection.",',
|
|
6222
|
+
' "Third quotable line that sets the next-step expectation."',
|
|
6223
|
+
" ]",
|
|
6224
|
+
" },",
|
|
6225
|
+
' "conclusion": "One-sentence bottom-line takeaway \xB7 \u2264 100 chars.",',
|
|
6226
|
+
' "flow": null,',
|
|
6227
|
+
' "footerTag": "{date} \xB7 Edition 01"',
|
|
6228
|
+
"}",
|
|
6229
|
+
"```",
|
|
6230
|
+
"",
|
|
6231
|
+
"Constraints:",
|
|
6232
|
+
"\xB7 Title MUST be declarative claim-form (newspaper headlines DO NOT ask questions on the front page \xB7 they STATE).",
|
|
6233
|
+
"\xB7 Milestones bodies are LONGER than other modes (4-7 sentences) \xB7 this is a column, not a card.",
|
|
6234
|
+
'\xB7 Verification bullets MUST follow "Heading: body." with a colon separator \xB7 the renderer splits on it.',
|
|
6235
|
+
"\xB7 No markdown formatting inside string fields. No bullet characters. No headings. Plain prose only \u2014 the renderer adds visual structure."
|
|
6236
|
+
].join("\n");
|
|
6237
|
+
function buildNewspaperMessages(opts) {
|
|
6238
|
+
const { room, members, perDirectorSignals, language } = opts;
|
|
6239
|
+
const memberList = members.map((a) => `${a.id} \xB7 ${a.name} (${a.handle}) \u2014 ${a.roleTag}`).join("\n \xB7 ");
|
|
6240
|
+
const signalsBlock = perDirectorSignals.map((d) => {
|
|
6241
|
+
if (!d.signals.length) return `[${d.directorId}] ${d.directorName} \u2014 (no signals)`;
|
|
6242
|
+
const lines = d.signals.map((s, i) => ` \xB7 ${d.directorId}#${i} [${s.lens}] ${s.text}`).join("\n");
|
|
6243
|
+
return `[${d.directorId}] ${d.directorName}
|
|
6244
|
+
${lines}`;
|
|
6245
|
+
}).join("\n\n");
|
|
6246
|
+
const supplementBlock = opts.supplement && opts.supplement.trim() ? [
|
|
6247
|
+
``,
|
|
6248
|
+
`\u2500\u2500\u2500 SUPPLEMENTARY PERSPECTIVE FROM USER \u2500\u2500\u2500`,
|
|
5353
6249
|
``,
|
|
5354
|
-
`The
|
|
5355
|
-
...[...set].sort().map((k) => ` \xB7 ${k}`),
|
|
6250
|
+
`The user has asked you to additionally consider this angle when building the newspaper. Surface it in the most fitting slot (most often as one of the 3 milestone columns or as a verification headline).`,
|
|
5356
6251
|
``,
|
|
5357
|
-
|
|
5358
|
-
...skipped.sort().map((k) => ` \xB7 ${k}`),
|
|
6252
|
+
opts.supplement.trim(),
|
|
5359
6253
|
``,
|
|
5360
|
-
`\u2500\u2500\u2500 END
|
|
5361
|
-
].join("\n");
|
|
6254
|
+
`\u2500\u2500\u2500 END SUPPLEMENT \u2500\u2500\u2500`
|
|
6255
|
+
].join("\n") : "";
|
|
6256
|
+
return [
|
|
6257
|
+
{
|
|
6258
|
+
role: "system",
|
|
6259
|
+
content: [NEWSPAPER_SYSTEM, "", languageInstruction(language)].join("\n")
|
|
6260
|
+
},
|
|
6261
|
+
{
|
|
6262
|
+
role: "user",
|
|
6263
|
+
content: [
|
|
6264
|
+
`ROOM #${room.number} \xB7 ${room.name}`,
|
|
6265
|
+
`Subject: ${room.subject}`,
|
|
6266
|
+
``,
|
|
6267
|
+
`Directors:`,
|
|
6268
|
+
` \xB7 ${memberList}`,
|
|
6269
|
+
``,
|
|
6270
|
+
`\u2500\u2500\u2500 SIGNALS \u2500\u2500\u2500`,
|
|
6271
|
+
``,
|
|
6272
|
+
signalsBlock || "(no signals extracted)",
|
|
6273
|
+
``,
|
|
6274
|
+
`\u2500\u2500\u2500 END SIGNALS \u2500\u2500\u2500`,
|
|
6275
|
+
supplementBlock,
|
|
6276
|
+
``,
|
|
6277
|
+
`Produce the newspaper front page now. JSON only.`
|
|
6278
|
+
].join("\n")
|
|
6279
|
+
}
|
|
6280
|
+
];
|
|
5362
6281
|
}
|
|
5363
|
-
|
|
5364
|
-
|
|
6282
|
+
var PPT_SYSTEM = [
|
|
6283
|
+
"You are the chair of a boardroom session. You produce a SLIDE DECK report \u2014 a presentation that decomposes into 7-9 slides the reader scrolls through with arrow keys: cover \xB7 agenda \xB7 3 milestone slides \xB7 optional data slide \xB7 talking-points slide \xB7 what-to-watch slide \xB7 takeaway slide. Not a memo. Not a magazine. A DECK.",
|
|
6284
|
+
"",
|
|
6285
|
+
"## Voice",
|
|
6286
|
+
"",
|
|
6287
|
+
"Slide voice is COMPRESSED. Every line is a slide-line \xB7 short, claim-form, declarative. No essays inside slot bodies \xB7 the renderer paints each milestone body as bullets / a single short paragraph. If a sentence wouldn't fit on a slide line at 32px, cut it.",
|
|
6288
|
+
"",
|
|
6289
|
+
"Each milestone fills exactly ONE slide \xB7 each talking point is one slide-line \xB7 each verification entry is one slide-line \xB7 the conclusion is the closing slide's hero quote.",
|
|
6290
|
+
"",
|
|
6291
|
+
"## The 8 slots you fill (output is JSON only)",
|
|
6292
|
+
"",
|
|
6293
|
+
"1. **title** \xB7 the deck's cover title \xB7 \u2264 80 chars \xB7 one short claim-form sentence. Slide-friendly: short enough to render at 60px display type without wrapping more than 2 lines.",
|
|
6294
|
+
"",
|
|
6295
|
+
"2. **kicker** \xB7 cover deck / sub-title \xB7 1 sentence \u2264 160 chars. Sits below the cover title in italic register.",
|
|
6296
|
+
"",
|
|
6297
|
+
'3. **source** \xB7 cover byline \xB7 \u2264 80 chars \xB7 "From the desk of {chair name} \xB7 {date}" or similar. Auto-filled if you skip.',
|
|
6298
|
+
"",
|
|
6299
|
+
"4. **milestones** \xB7 EXACTLY 3 slide-stories \xB7 each fills ONE content slide. Each card has:",
|
|
6300
|
+
' \xB7 `period` \xB7 slide-section label \xB7 \u2264 24 chars \xB7 "PHASE 1" / "Q4 OUTLOOK" / "NEXT STEP". Slide-banner register \xB7 ALL-CAPS-ABLE.',
|
|
6301
|
+
" \xB7 `title` \xB7 slide headline \xB7 \u2264 60 chars \xB7 the slide's one-line claim.",
|
|
6302
|
+
' \xB7 `body` \xB7 2-3 sentences \xB7 \u2264 280 chars \xB7 short-form for slide rendering. The renderer may break this into 2-3 bullets at the colon / semicolon, so you can write "Three forces line up: pricing pressure, regulatory shift, talent gap." and the renderer will visualise it.',
|
|
6303
|
+
' \xB7 `callout` \xB7 short numeric \xB7 \u2264 12 chars \xB7 "-10\xD7" / "$120M" / "Q1 2026" \xB7 used as the slide\'s stat callout when present. Empty when no clean numeric anchor.',
|
|
6304
|
+
" \xB7 `tags` \xB7 empty array \xB7 the slide layout doesn't render tags.",
|
|
6305
|
+
"",
|
|
6306
|
+
"5. **rankedBars** \xB7 OPTIONAL \xB7 the 'By the numbers' slide \xB7 3-5 ranked entries with normalised ratio bars. Set null when the room has no clean ranked-numeric material.",
|
|
6307
|
+
"",
|
|
6308
|
+
`6. **verification** \xB7 MANDATORY \xB7 the 'What to watch' slide \xB7 3-5 entries \xB7 each \u2264 100 chars \xB7 phrased as a SINGLE-LINE bullet ("Compliance review remains gating constraint."). NO "Heading: body" colon split here \xB7 these are slide bullets, one line each.`,
|
|
6309
|
+
"",
|
|
6310
|
+
`7. **talkingPoints** \xB7 MANDATORY \xB7 the 'Recommendations / Talking points' slide \xB7 3-5 entries \xB7 each \u2264 80 chars \xB7 imperative single-line bullets a presenter could read aloud ("Lock pricing for Q1." / "Move release to two-track plan.").`,
|
|
6311
|
+
"",
|
|
6312
|
+
"8. **conclusion** \xB7 the closing 'Takeaway' slide \xB7 \u2264 100 chars \xB7 ONE sentence \xB7 the deck's big walk-away line \xB7 rendered at 40-60px display type.",
|
|
6313
|
+
"",
|
|
6314
|
+
"9. **directorBlock** \xB7 MANDATORY when the room has \u2265 2 active directors. Set `null` only when there is exactly one director. Renders as 1-3 dedicated slides: director voices \xB7 where we agreed \xB7 where we split. The single most-valuable section in the deck \u2014 the room's social map. Shape:",
|
|
6315
|
+
" \xB7 `perspectives` \xB7 EVERY active director gets one entry. Each has:",
|
|
6316
|
+
' \xB7 `directorName` (display name, \u2264 32 chars \xB7 e.g. "Socrates"),',
|
|
6317
|
+
' \xB7 `directorRole` (their tag, \u2264 48 chars \xB7 e.g. "Devil\'s advocate"),',
|
|
6318
|
+
' \xB7 `stance` (\u2264 60 chars \xB7 short label of their angle \xB7 "Sees this as a moat play"),',
|
|
6319
|
+
" \xB7 `position` (1-2 sentences \u2264 240 chars \xB7 their load-bearing argument in their voice),",
|
|
6320
|
+
" \xB7 `quote` (verbatim phrase \u2264 160 chars \xB7 empty when no memorable verbatim).",
|
|
6321
|
+
" \xB7 `alignment` \xB7 0-3 cross-cutting agreements. Each: `pointOfAgreement` (\u2264 120 chars), `directorNames` (\u2265 2 names), `note` (why this convergence matters \xB7 \u2264 200 chars).",
|
|
6322
|
+
" \xB7 `divergence` \xB7 0-2 hinges the room split on. Each: `hinge` (\u2264 140 chars \xB7 the question they split on), `sides` (2-3 entries \xB7 each has `label`, `directorNames` \u2265 1, `stance` \u2264 160 chars), `resolution` (what would settle it \xB7 \u2264 200 chars \xB7 empty if unresolved).",
|
|
6323
|
+
" \xB7 `chairSynthesis` \xB7 1-2 sentences \u2264 240 chars \xB7 what the chair takes from comparing the views. Moderator-neutral \u2014 observation, not advocacy.",
|
|
6324
|
+
" Source from the SIGNALS block \u2014 `tensions` populate divergence rows; agreed-on claims populate alignment groups; each director's strongest claim populates their `position`; quotes from the SIGNALS block (when verbatim) populate `quote`.",
|
|
6325
|
+
"",
|
|
6326
|
+
"Plus optional **flow** \xB7 usually `null` in PPT mode \xB7 only fill when the room argued a clean transformation arc \xB7 would render as a 2-4 node arrow chain on the data slide.",
|
|
6327
|
+
"",
|
|
6328
|
+
"Plus auto **footerTag** \xB7 slide-footer caption \xB7 auto-filled if you skip.",
|
|
6329
|
+
"",
|
|
6330
|
+
"## Routing the SIGNALS block into slide slots",
|
|
6331
|
+
"",
|
|
6332
|
+
" \xB7 **title** \u2190 the strongest claim \xB7 phrased as a deck cover headline.",
|
|
6333
|
+
" \xB7 **kicker** \u2190 the supporting sub-title \xB7 what the deck is about in 1 sentence.",
|
|
6334
|
+
" \xB7 **milestones** \u2190 the 3 most load-bearing pieces \xB7 each becomes ONE slide \xB7 prefer claims with numeric anchors so the slide gets a visible callout.",
|
|
6335
|
+
" \xB7 **verification** \u2190 3-5 watch-list bullets \xB7 single-line each.",
|
|
6336
|
+
" \xB7 **talkingPoints** \u2190 3-5 imperative recommendations \xB7 single-line each.",
|
|
6337
|
+
" \xB7 **conclusion** \u2190 compressed walk-away takeaway in 1 sentence.",
|
|
6338
|
+
"",
|
|
6339
|
+
"## Output format",
|
|
6340
|
+
"",
|
|
6341
|
+
"Strict JSON inside a fenced ```json code block. No prose outside the block.",
|
|
6342
|
+
"",
|
|
6343
|
+
"```json",
|
|
6344
|
+
"{",
|
|
6345
|
+
' "title": "Short claim-form deck title.",',
|
|
6346
|
+
' "kicker": "1-sentence sub-title explaining the angle.",',
|
|
6347
|
+
' "source": "From the desk of {chair name} \xB7 {date}",',
|
|
6348
|
+
' "milestones": [',
|
|
6349
|
+
' { "period": "PHASE 1", "title": "Anchor the next quarter", "body": "Three forces converge: pricing pressure, regulatory shift, talent gap.", "callout": "Q1 2026", "tags": [] },',
|
|
6350
|
+
' { "period": "PHASE 2", "title": "Commit two-track release", "body": "Pilot lane survives. Production lane gets a tighter cap.", "callout": "-10\xD7", "tags": [] },',
|
|
6351
|
+
' { "period": "PHASE 3", "title": "Open the senior bench", "body": "Two roles before next board \xB7 compliance review remains the gate.", "callout": "+2 hires", "tags": [] }',
|
|
6352
|
+
" ],",
|
|
6353
|
+
' "rankedBars": null,',
|
|
6354
|
+
' "verification": {',
|
|
6355
|
+
' "title": "What to watch",',
|
|
6356
|
+
' "bullets": [',
|
|
6357
|
+
' "Compliance review remains the gating constraint.",',
|
|
6358
|
+
' "Pricing pilot survives but caps are tightening.",',
|
|
6359
|
+
' "Two new senior roles open by next board meeting.",',
|
|
6360
|
+
' "Q4 outlook anchors three commitments."',
|
|
6361
|
+
" ]",
|
|
6362
|
+
" },",
|
|
6363
|
+
' "talkingPoints": {',
|
|
6364
|
+
' "title": "Recommendations",',
|
|
6365
|
+
' "bullets": [',
|
|
6366
|
+
' "Lock pricing for Q1.",',
|
|
6367
|
+
' "Commit to a two-track release plan.",',
|
|
6368
|
+
' "Open two senior roles before the next board.",',
|
|
6369
|
+
' "Schedule a compliance review check-in for week 4."',
|
|
6370
|
+
" ]",
|
|
6371
|
+
" },",
|
|
6372
|
+
' "conclusion": "Three commitments anchor the next quarter; ship Q1 with pricing locked.",',
|
|
6373
|
+
' "flow": null,',
|
|
6374
|
+
' "footerTag": "Boardroom Slides \xB7 {date}",',
|
|
6375
|
+
' "directorBlock": {',
|
|
6376
|
+
' "perspectives": [',
|
|
6377
|
+
` { "directorName": "Socrates", "directorRole": "Devil's advocate", "stance": "Frames it as a regulatory-window question.", "position": "The compliance gate is binding before the pricing question matters. Until that closes, two-track is overkill.", "quote": "We are spending capital on a problem that is not our binding constraint." },`,
|
|
6378
|
+
` { "directorName": "Drucker", "directorRole": "Operator's lens", "stance": "Reads it as a distribution-leverage play.", "position": "Two-track buys negotiating room with the channel \u2014 without it, Q1 pricing locks become impossible to reverse.", "quote": "" }`,
|
|
6379
|
+
" ],",
|
|
6380
|
+
' "alignment": [',
|
|
6381
|
+
' { "pointOfAgreement": "Q1 pricing must be locked before any release commitment.", "directorNames": ["Socrates", "Drucker"], "note": "Independent paths \xB7 Socrates from regulatory exposure, Drucker from channel leverage." }',
|
|
6382
|
+
" ],",
|
|
6383
|
+
' "divergence": [',
|
|
6384
|
+
' { "hinge": "Whether two-track release is necessary now or premature.", "sides": [',
|
|
6385
|
+
' { "label": "Necessary now", "directorNames": ["Drucker"], "stance": "Without two-track, the pricing lock is reversed by channel pressure inside 90 days." },',
|
|
6386
|
+
' { "label": "Premature", "directorNames": ["Socrates"], "stance": "Compliance review, not channel pressure, is the binding gate; two-track defers the real question." }',
|
|
6387
|
+
' ], "resolution": "Resolves once we know whether the compliance review will close before the pricing lock takes effect." }',
|
|
6388
|
+
" ],",
|
|
6389
|
+
' "chairSynthesis": "Both directors agree on the pricing lock; they split on whether to also commit to two-track. The compliance-review timing is the variable that settles the split."',
|
|
6390
|
+
" }",
|
|
6391
|
+
"}",
|
|
6392
|
+
"```",
|
|
6393
|
+
"",
|
|
6394
|
+
"Constraints:",
|
|
6395
|
+
"\xB7 Title \u2264 80 chars \xB7 slide-renderable at 60px without overflow.",
|
|
6396
|
+
"\xB7 Milestone body 2-3 sentences \u2264 280 chars \xB7 TIGHTER than newspaper / magazine modes.",
|
|
6397
|
+
"\xB7 Verification bullets \u2264 100 chars each \xB7 single-line slide-bullets \xB7 NO colon split.",
|
|
6398
|
+
"\xB7 Talking-point bullets \u2264 80 chars each \xB7 imperative voice.",
|
|
6399
|
+
"\xB7 Conclusion \u2264 100 chars \xB7 the deck's walk-away line.",
|
|
6400
|
+
"\xB7 directorBlock \u2014 when there are \u2265 2 active directors, you MUST populate it with one entry per director. Do not skip it; the deck loses its core differentiator without these slides.",
|
|
6401
|
+
"\xB7 No markdown formatting inside string fields. No bullet characters. No headings. Plain prose only \u2014 the renderer adds visual structure."
|
|
6402
|
+
].join("\n");
|
|
6403
|
+
function buildPptMessages(opts) {
|
|
6404
|
+
const { room, members, perDirectorSignals, language } = opts;
|
|
5365
6405
|
const memberList = members.map((a) => `${a.id} \xB7 ${a.name} (${a.handle}) \u2014 ${a.roleTag}`).join("\n \xB7 ");
|
|
5366
6406
|
const signalsBlock = perDirectorSignals.map((d) => {
|
|
5367
6407
|
if (!d.signals.length) return `[${d.directorId}] ${d.directorName} \u2014 (no signals)`;
|
|
5368
|
-
const lines = d.signals.map(
|
|
5369
|
-
(s, i) => ` \xB7 ${d.directorId}#${i} [${s.lens}] ${s.text}`
|
|
5370
|
-
).join("\n");
|
|
6408
|
+
const lines = d.signals.map((s, i) => ` \xB7 ${d.directorId}#${i} [${s.lens}] ${s.text}`).join("\n");
|
|
5371
6409
|
return `[${d.directorId}] ${d.directorName}
|
|
5372
6410
|
${lines}`;
|
|
5373
6411
|
}).join("\n\n");
|
|
@@ -5375,7 +6413,7 @@ ${lines}`;
|
|
|
5375
6413
|
``,
|
|
5376
6414
|
`\u2500\u2500\u2500 SUPPLEMENTARY PERSPECTIVE FROM USER \u2500\u2500\u2500`,
|
|
5377
6415
|
``,
|
|
5378
|
-
`The user has asked you to additionally consider this angle when building the
|
|
6416
|
+
`The user has asked you to additionally consider this angle when building the deck. Surface it in the most fitting slot (most often as one of the 3 milestone slides or as a watch-list bullet).`,
|
|
5379
6417
|
``,
|
|
5380
6418
|
opts.supplement.trim(),
|
|
5381
6419
|
``,
|
|
@@ -5384,18 +6422,13 @@ ${lines}`;
|
|
|
5384
6422
|
return [
|
|
5385
6423
|
{
|
|
5386
6424
|
role: "system",
|
|
5387
|
-
content: [
|
|
6425
|
+
content: [PPT_SYSTEM, "", languageInstruction(language)].join("\n")
|
|
5388
6426
|
},
|
|
5389
6427
|
{
|
|
5390
6428
|
role: "user",
|
|
5391
6429
|
content: [
|
|
5392
6430
|
`ROOM #${room.number} \xB7 ${room.name}`,
|
|
5393
6431
|
`Subject: ${room.subject}`,
|
|
5394
|
-
// `Mode: …` deliberately omitted · see composer.ts for the
|
|
5395
|
-
// same change. Surfacing the room mode here biased the LLM
|
|
5396
|
-
// toward critique-shaped / brainstorm-shaped output even
|
|
5397
|
-
// though the standard scaffold prompt asks for decision-grade
|
|
5398
|
-
// JSON, leading to parseScaffold rejecting every retry.
|
|
5399
6432
|
``,
|
|
5400
6433
|
`Directors:`,
|
|
5401
6434
|
` \xB7 ${memberList}`,
|
|
@@ -5405,10 +6438,9 @@ ${lines}`;
|
|
|
5405
6438
|
signalsBlock || "(no signals extracted)",
|
|
5406
6439
|
``,
|
|
5407
6440
|
`\u2500\u2500\u2500 END SIGNALS \u2500\u2500\u2500`,
|
|
5408
|
-
pickedBlock(picked),
|
|
5409
6441
|
supplementBlock,
|
|
5410
6442
|
``,
|
|
5411
|
-
`Produce the
|
|
6443
|
+
`Produce the slide deck now. JSON only.`
|
|
5412
6444
|
].join("\n")
|
|
5413
6445
|
}
|
|
5414
6446
|
];
|
|
@@ -7720,6 +8752,188 @@ function parseScaffold(raw, fallbackTitle, fallbackOriginalQuestion) {
|
|
|
7720
8752
|
openQuestions: parseOpenQuestions(parsed.openQuestions)
|
|
7721
8753
|
};
|
|
7722
8754
|
}
|
|
8755
|
+
function parseBento(raw, fallbackTitle, fallbackSource, fallbackFooterTag) {
|
|
8756
|
+
const parsed = extractJson3(raw);
|
|
8757
|
+
if (!parsed) return null;
|
|
8758
|
+
const title = clipString(stringField(parsed.title) || fallbackTitle, 110);
|
|
8759
|
+
if (!title) return null;
|
|
8760
|
+
const kicker = clipString(stringField(parsed.kicker), 200);
|
|
8761
|
+
const source = clipString(stringField(parsed.source) || fallbackSource, 80);
|
|
8762
|
+
const milestones = parseBentoMilestones(parsed.milestones);
|
|
8763
|
+
if (milestones.length === 0) return null;
|
|
8764
|
+
while (milestones.length < 3) {
|
|
8765
|
+
milestones.push({ period: "", title: "", body: "", callout: "", tags: [] });
|
|
8766
|
+
}
|
|
8767
|
+
if (milestones.length > 3) milestones.length = 3;
|
|
8768
|
+
const rankedBars = parseBentoRankedBars(parsed.rankedBars);
|
|
8769
|
+
const verification = parseBentoVerification(parsed.verification);
|
|
8770
|
+
const talkingPoints = parseBentoTalkingPoints(parsed.talkingPoints);
|
|
8771
|
+
const conclusion = clipString(stringField(parsed.conclusion), 100);
|
|
8772
|
+
const flow = parseBentoFlow(parsed.flow);
|
|
8773
|
+
const footerTag = clipString(stringField(parsed.footerTag) || fallbackFooterTag, 80);
|
|
8774
|
+
const directorBlock = parsePptDirectorBlock(parsed.directorBlock);
|
|
8775
|
+
return {
|
|
8776
|
+
title,
|
|
8777
|
+
kicker,
|
|
8778
|
+
source,
|
|
8779
|
+
milestones,
|
|
8780
|
+
rankedBars,
|
|
8781
|
+
verification,
|
|
8782
|
+
talkingPoints,
|
|
8783
|
+
conclusion,
|
|
8784
|
+
flow,
|
|
8785
|
+
footerTag,
|
|
8786
|
+
directorBlock
|
|
8787
|
+
};
|
|
8788
|
+
}
|
|
8789
|
+
function parsePptDirectorBlock(raw) {
|
|
8790
|
+
if (!raw || typeof raw !== "object") return null;
|
|
8791
|
+
const o = raw;
|
|
8792
|
+
const perspectives = [];
|
|
8793
|
+
if (Array.isArray(o.perspectives)) {
|
|
8794
|
+
for (const p of o.perspectives) {
|
|
8795
|
+
if (!p || typeof p !== "object") continue;
|
|
8796
|
+
const po = p;
|
|
8797
|
+
const directorName = clipString(stringField(po.directorName), 32);
|
|
8798
|
+
const directorRole = clipString(stringField(po.directorRole), 48);
|
|
8799
|
+
const stance = clipString(stringField(po.stance), 60);
|
|
8800
|
+
const position = clipString(stringField(po.position), 240);
|
|
8801
|
+
if (!directorName || !position) continue;
|
|
8802
|
+
const quote = clipString(stringField(po.quote), 160);
|
|
8803
|
+
perspectives.push({ directorName, directorRole, stance, position, quote });
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
if (perspectives.length < 2) return null;
|
|
8807
|
+
const alignment = [];
|
|
8808
|
+
if (Array.isArray(o.alignment)) {
|
|
8809
|
+
for (const a of o.alignment) {
|
|
8810
|
+
if (!a || typeof a !== "object") continue;
|
|
8811
|
+
const ao = a;
|
|
8812
|
+
const pointOfAgreement = clipString(stringField(ao.pointOfAgreement), 120);
|
|
8813
|
+
const note = clipString(stringField(ao.note), 200);
|
|
8814
|
+
const namesRaw = Array.isArray(ao.directorNames) ? ao.directorNames : [];
|
|
8815
|
+
const directorNames = namesRaw.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => clipString(s.trim(), 32)).slice(0, 6);
|
|
8816
|
+
if (!pointOfAgreement || directorNames.length < 2) continue;
|
|
8817
|
+
alignment.push({ pointOfAgreement, directorNames, note });
|
|
8818
|
+
if (alignment.length >= 3) break;
|
|
8819
|
+
}
|
|
8820
|
+
}
|
|
8821
|
+
const divergence = [];
|
|
8822
|
+
if (Array.isArray(o.divergence)) {
|
|
8823
|
+
for (const d of o.divergence) {
|
|
8824
|
+
if (!d || typeof d !== "object") continue;
|
|
8825
|
+
const dgo = d;
|
|
8826
|
+
const hinge = clipString(stringField(dgo.hinge), 140);
|
|
8827
|
+
const resolution = clipString(stringField(dgo.resolution), 200);
|
|
8828
|
+
const sides = [];
|
|
8829
|
+
if (Array.isArray(dgo.sides)) {
|
|
8830
|
+
for (const s of dgo.sides) {
|
|
8831
|
+
if (!s || typeof s !== "object") continue;
|
|
8832
|
+
const so = s;
|
|
8833
|
+
const label = clipString(stringField(so.label), 40);
|
|
8834
|
+
const stance = clipString(stringField(so.stance), 160);
|
|
8835
|
+
const namesRaw = Array.isArray(so.directorNames) ? so.directorNames : [];
|
|
8836
|
+
const names = namesRaw.filter((x) => typeof x === "string" && x.trim().length > 0).map((x) => clipString(x.trim(), 32)).slice(0, 4);
|
|
8837
|
+
if (!label || !stance || names.length === 0) continue;
|
|
8838
|
+
sides.push({ label, directorNames: names, stance });
|
|
8839
|
+
}
|
|
8840
|
+
}
|
|
8841
|
+
if (!hinge || sides.length < 2) continue;
|
|
8842
|
+
divergence.push({ hinge, sides, resolution });
|
|
8843
|
+
if (divergence.length >= 2) break;
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
const chairSynthesis = clipString(stringField(o.chairSynthesis), 240);
|
|
8847
|
+
return { perspectives, alignment, divergence, chairSynthesis };
|
|
8848
|
+
}
|
|
8849
|
+
function stringField(v) {
|
|
8850
|
+
return typeof v === "string" ? v.trim() : "";
|
|
8851
|
+
}
|
|
8852
|
+
function clipString(s, max) {
|
|
8853
|
+
if (s.length <= max) return s;
|
|
8854
|
+
return s.slice(0, max - 1).trimEnd() + "\u2026";
|
|
8855
|
+
}
|
|
8856
|
+
function parseBentoMilestones(raw) {
|
|
8857
|
+
if (!Array.isArray(raw)) return [];
|
|
8858
|
+
const out = [];
|
|
8859
|
+
for (const m of raw) {
|
|
8860
|
+
if (!m || typeof m !== "object") continue;
|
|
8861
|
+
const o = m;
|
|
8862
|
+
const title = clipString(stringField(o.title), 60);
|
|
8863
|
+
const body = clipString(stringField(o.body), 220);
|
|
8864
|
+
if (!title || !body) continue;
|
|
8865
|
+
const period = clipString(stringField(o.period), 24);
|
|
8866
|
+
const callout = clipString(stringField(o.callout), 12);
|
|
8867
|
+
const tagsRaw = Array.isArray(o.tags) ? o.tags : [];
|
|
8868
|
+
const tags = tagsRaw.map((t) => typeof t === "string" ? clipString(t.trim(), 16) : "").filter(Boolean).slice(0, 4);
|
|
8869
|
+
out.push({ period, title, body, callout, tags });
|
|
8870
|
+
if (out.length >= 3) break;
|
|
8871
|
+
}
|
|
8872
|
+
return out;
|
|
8873
|
+
}
|
|
8874
|
+
function parseBentoRankedBars(raw) {
|
|
8875
|
+
if (!raw || typeof raw !== "object") return null;
|
|
8876
|
+
const o = raw;
|
|
8877
|
+
const title = clipString(stringField(o.title), 40);
|
|
8878
|
+
if (!title) return null;
|
|
8879
|
+
const entriesRaw = Array.isArray(o.entries) ? o.entries : [];
|
|
8880
|
+
const entries = [];
|
|
8881
|
+
for (const e of entriesRaw) {
|
|
8882
|
+
if (!e || typeof e !== "object") continue;
|
|
8883
|
+
const eo = e;
|
|
8884
|
+
const label = clipString(stringField(eo.label), 40);
|
|
8885
|
+
const value = clipString(stringField(eo.value), 20);
|
|
8886
|
+
if (!label || !value) continue;
|
|
8887
|
+
const ratioRaw = typeof eo.ratio === "number" ? eo.ratio : 0;
|
|
8888
|
+
const ratio = Math.max(0, Math.min(1, Number.isFinite(ratioRaw) ? ratioRaw : 0));
|
|
8889
|
+
entries.push({ label, value, ratio });
|
|
8890
|
+
if (entries.length >= 5) break;
|
|
8891
|
+
}
|
|
8892
|
+
if (entries.length < 2) return null;
|
|
8893
|
+
return { title, entries };
|
|
8894
|
+
}
|
|
8895
|
+
function parseBentoVerification(raw) {
|
|
8896
|
+
if (!raw || typeof raw !== "object") return null;
|
|
8897
|
+
const o = raw;
|
|
8898
|
+
const title = clipString(stringField(o.title), 40);
|
|
8899
|
+
if (!title) return null;
|
|
8900
|
+
const bullets = parseBentoBullets(o.bullets, 140, 5);
|
|
8901
|
+
if (bullets.length === 0) return null;
|
|
8902
|
+
return { title, bullets };
|
|
8903
|
+
}
|
|
8904
|
+
function parseBentoTalkingPoints(raw) {
|
|
8905
|
+
if (!raw || typeof raw !== "object") {
|
|
8906
|
+
return { title: "How to say this", bullets: [] };
|
|
8907
|
+
}
|
|
8908
|
+
const o = raw;
|
|
8909
|
+
const title = clipString(stringField(o.title), 40) || "How to say this";
|
|
8910
|
+
const bullets = parseBentoBullets(o.bullets, 120, 5);
|
|
8911
|
+
return { title, bullets };
|
|
8912
|
+
}
|
|
8913
|
+
function parseBentoBullets(raw, maxChars, maxCount) {
|
|
8914
|
+
if (!Array.isArray(raw)) return [];
|
|
8915
|
+
const out = [];
|
|
8916
|
+
for (const b of raw) {
|
|
8917
|
+
const s = typeof b === "string" ? clipString(b.trim(), maxChars) : "";
|
|
8918
|
+
if (s) out.push(s);
|
|
8919
|
+
if (out.length >= maxCount) break;
|
|
8920
|
+
}
|
|
8921
|
+
return out;
|
|
8922
|
+
}
|
|
8923
|
+
function parseBentoFlow(raw) {
|
|
8924
|
+
if (!raw || typeof raw !== "object") return null;
|
|
8925
|
+
const o = raw;
|
|
8926
|
+
const nodesRaw = Array.isArray(o.nodes) ? o.nodes : [];
|
|
8927
|
+
const nodes = [];
|
|
8928
|
+
for (const n of nodesRaw) {
|
|
8929
|
+
const s = typeof n === "string" ? clipString(n.trim(), 24) : "";
|
|
8930
|
+
if (s) nodes.push(s);
|
|
8931
|
+
if (nodes.length >= 4) break;
|
|
8932
|
+
}
|
|
8933
|
+
if (nodes.length < 2) return null;
|
|
8934
|
+
const caption = clipString(stringField(o.caption), 60);
|
|
8935
|
+
return caption ? { nodes, caption } : { nodes };
|
|
8936
|
+
}
|
|
7723
8937
|
function parseAppendices(raw) {
|
|
7724
8938
|
if (!Array.isArray(raw)) return null;
|
|
7725
8939
|
const out = [];
|
|
@@ -8580,7 +9794,7 @@ function recoverStuckClarifyRooms() {
|
|
|
8580
9794
|
|
|
8581
9795
|
// src/storage/briefs.ts
|
|
8582
9796
|
init_db();
|
|
8583
|
-
var COLS2 = "id, room_id, style, title, body_md, body_json, supplement, spine, components_json, composer_rationale, subject_type, house_style, assets_json, created_at";
|
|
9797
|
+
var COLS2 = "id, room_id, style, title, body_md, body_json, supplement, spine, components_json, composer_rationale, subject_type, house_style, assets_json, mode, created_at";
|
|
8584
9798
|
function parseAssets(json) {
|
|
8585
9799
|
if (!json) return null;
|
|
8586
9800
|
try {
|
|
@@ -8761,6 +9975,11 @@ function mapRow7(row) {
|
|
|
8761
9975
|
subjectType: row.subject_type,
|
|
8762
9976
|
houseStyle: row.house_style || "boardroom-default",
|
|
8763
9977
|
assets: parseAssets(row.assets_json),
|
|
9978
|
+
// Legacy 'bento' rows are mapped to 'magazine' · the body_json
|
|
9979
|
+
// shape is identical (BentoScaffold) and the magazine renderer
|
|
9980
|
+
// handles the data correctly. Unknown modes fall back to the
|
|
9981
|
+
// default research-note path.
|
|
9982
|
+
mode: row.mode === "magazine" || row.mode === "newspaper" || row.mode === "ppt" ? row.mode : row.mode === "bento" ? "magazine" : "research-note",
|
|
8764
9983
|
createdAt: row.created_at
|
|
8765
9984
|
};
|
|
8766
9985
|
}
|
|
@@ -8780,7 +9999,7 @@ function listAllBriefs() {
|
|
|
8780
9999
|
const rows = getDb().prepare(
|
|
8781
10000
|
`SELECT b.id, b.room_id, b.style, b.title, b.body_md, b.body_json,
|
|
8782
10001
|
b.supplement, b.spine, b.components_json,
|
|
8783
|
-
b.composer_rationale, b.subject_type, b.house_style, b.assets_json, b.created_at,
|
|
10002
|
+
b.composer_rationale, b.subject_type, b.house_style, b.assets_json, b.mode, b.created_at,
|
|
8784
10003
|
r.name AS room_name, r.subject AS room_subject,
|
|
8785
10004
|
r.number AS room_number, r.status AS room_status
|
|
8786
10005
|
FROM briefs b
|
|
@@ -8806,8 +10025,9 @@ function insertBrief(b) {
|
|
|
8806
10025
|
const composerRationale = b.composerRationale && b.composerRationale.trim() ? b.composerRationale.trim() : null;
|
|
8807
10026
|
const subjectType = b.subjectType && b.subjectType.trim() ? b.subjectType.trim() : null;
|
|
8808
10027
|
const houseStyle = b.houseStyle && b.houseStyle.trim() ? b.houseStyle.trim() : "boardroom-default";
|
|
10028
|
+
const mode = b.mode === "magazine" || b.mode === "newspaper" || b.mode === "ppt" ? b.mode : "research-note";
|
|
8809
10029
|
db.prepare(
|
|
8810
|
-
`INSERT INTO briefs (${COLS2}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
10030
|
+
`INSERT INTO briefs (${COLS2}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
8811
10031
|
).run(
|
|
8812
10032
|
id,
|
|
8813
10033
|
b.roomId,
|
|
@@ -8823,6 +10043,7 @@ function insertBrief(b) {
|
|
|
8823
10043
|
houseStyle,
|
|
8824
10044
|
null,
|
|
8825
10045
|
// assets_json · filled later by updateBriefAssets
|
|
10046
|
+
mode,
|
|
8826
10047
|
now
|
|
8827
10048
|
);
|
|
8828
10049
|
return getBrief(id);
|
|
@@ -8868,12 +10089,22 @@ function updateBriefBody(id, bodyMd, title) {
|
|
|
8868
10089
|
function setBriefTitle(id, title) {
|
|
8869
10090
|
getDb().prepare("UPDATE briefs SET title = ? WHERE id = ?").run(title, id);
|
|
8870
10091
|
}
|
|
10092
|
+
function updateBriefBodyJson(id, bodyJson, title) {
|
|
10093
|
+
const json = JSON.stringify(bodyJson);
|
|
10094
|
+
if (title !== void 0) {
|
|
10095
|
+
getDb().prepare("UPDATE briefs SET body_json = ?, title = ? WHERE id = ?").run(json, title, id);
|
|
10096
|
+
} else {
|
|
10097
|
+
getDb().prepare("UPDATE briefs SET body_json = ? WHERE id = ?").run(json, id);
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
8871
10100
|
function deleteBrief(id) {
|
|
8872
10101
|
const r = getDb().prepare("DELETE FROM briefs WHERE id = ?").run(id);
|
|
8873
10102
|
return r.changes > 0;
|
|
8874
10103
|
}
|
|
8875
10104
|
function countBriefs() {
|
|
8876
|
-
const row = getDb().prepare(
|
|
10105
|
+
const row = getDb().prepare(
|
|
10106
|
+
"SELECT COUNT(*) AS c FROM briefs WHERE (body_md IS NOT NULL AND TRIM(body_md) != '') OR (body_json IS NOT NULL AND TRIM(body_json) != '' AND TRIM(body_json) != 'null')"
|
|
10107
|
+
).get();
|
|
8877
10108
|
return row.c ?? 0;
|
|
8878
10109
|
}
|
|
8879
10110
|
|
|
@@ -8982,6 +10213,7 @@ function abortBriefGeneration(briefId) {
|
|
|
8982
10213
|
async function generateBrief(opts) {
|
|
8983
10214
|
const { roomId } = opts;
|
|
8984
10215
|
const style = opts.style ?? "mckinsey";
|
|
10216
|
+
const mode = opts.mode === "magazine" || opts.mode === "newspaper" || opts.mode === "ppt" ? opts.mode : "research-note";
|
|
8985
10217
|
const room = getRoom(roomId);
|
|
8986
10218
|
if (!room) throw new Error(`room not found: ${roomId}`);
|
|
8987
10219
|
const memberRows = listRoomMembers(roomId);
|
|
@@ -8992,7 +10224,8 @@ async function generateBrief(opts) {
|
|
|
8992
10224
|
style,
|
|
8993
10225
|
title: room.subject,
|
|
8994
10226
|
bodyMd: "",
|
|
8995
|
-
supplement: opts.supplement
|
|
10227
|
+
supplement: opts.supplement,
|
|
10228
|
+
mode
|
|
8996
10229
|
});
|
|
8997
10230
|
const chairForState = getChairAgent();
|
|
8998
10231
|
const inferredLang = /[一-鿿]/.test(room.subject || "") ? "zh" : "en";
|
|
@@ -9011,6 +10244,7 @@ async function generateBrief(opts) {
|
|
|
9011
10244
|
briefId: placeholder.id,
|
|
9012
10245
|
roomId,
|
|
9013
10246
|
style,
|
|
10247
|
+
mode,
|
|
9014
10248
|
members,
|
|
9015
10249
|
transcript,
|
|
9016
10250
|
room,
|
|
@@ -9154,7 +10388,7 @@ async function runPipeline(args) {
|
|
|
9154
10388
|
roomBus.emit(roomId, {
|
|
9155
10389
|
type: "config-event",
|
|
9156
10390
|
kind: "brief-started",
|
|
9157
|
-
payload: { briefId, style, chairName, language },
|
|
10391
|
+
payload: { briefId, style, chairName, language, mode: args.mode },
|
|
9158
10392
|
createdAt: Date.now()
|
|
9159
10393
|
});
|
|
9160
10394
|
let buf = "";
|
|
@@ -9247,6 +10481,26 @@ async function runPipeline(args) {
|
|
|
9247
10481
|
`
|
|
9248
10482
|
);
|
|
9249
10483
|
}
|
|
10484
|
+
if (args.mode === "magazine" || args.mode === "newspaper" || args.mode === "ppt") {
|
|
10485
|
+
const ok = await runBentoStage({
|
|
10486
|
+
roomId,
|
|
10487
|
+
briefId,
|
|
10488
|
+
chair,
|
|
10489
|
+
chairId,
|
|
10490
|
+
room,
|
|
10491
|
+
members,
|
|
10492
|
+
perDirectorSignals,
|
|
10493
|
+
language,
|
|
10494
|
+
supplement,
|
|
10495
|
+
mode: args.mode,
|
|
10496
|
+
signal: args.signal
|
|
10497
|
+
});
|
|
10498
|
+
if (!ok) {
|
|
10499
|
+
const label = args.mode === "magazine" ? "Magazine" : args.mode === "ppt" ? "Slide deck" : "Newspaper";
|
|
10500
|
+
pipelineError = `${label} writer couldn't structure this room (3 retries failed). Try regenerating, or shorten the conversation.`;
|
|
10501
|
+
}
|
|
10502
|
+
return;
|
|
10503
|
+
}
|
|
9250
10504
|
const stage1ActualSec = (Date.now() - stage1StartedAt) / 1e3;
|
|
9251
10505
|
const stage1PredictedMid = (stage1Eta.lo + stage1Eta.hi) / 2;
|
|
9252
10506
|
let calibration = stage1PredictedMid > 0.5 ? stage1ActualSec / stage1PredictedMid : 1;
|
|
@@ -9675,6 +10929,90 @@ async function runStage2(args) {
|
|
|
9675
10929
|
}
|
|
9676
10930
|
return null;
|
|
9677
10931
|
}
|
|
10932
|
+
async function runBentoStage(args) {
|
|
10933
|
+
const totalSignals = args.perDirectorSignals.reduce(
|
|
10934
|
+
(acc, d) => acc + d.signals.length,
|
|
10935
|
+
0
|
|
10936
|
+
);
|
|
10937
|
+
if (totalSignals === 0) return false;
|
|
10938
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
10939
|
+
const chairName = args.chair?.name || "Chair";
|
|
10940
|
+
const fallbackSource = `From ${chairName} \xB7 ${today}`;
|
|
10941
|
+
const subjectShort = (args.room.subject || "").slice(0, 60);
|
|
10942
|
+
const fallbackFooterTag = subjectShort ? `${subjectShort} \xB7 ${today}` : today;
|
|
10943
|
+
const buildMessages = args.mode === "newspaper" ? buildNewspaperMessages : args.mode === "ppt" ? buildPptMessages : buildMagazineMessages;
|
|
10944
|
+
const messages = buildMessages({
|
|
10945
|
+
chair: args.chair,
|
|
10946
|
+
room: args.room,
|
|
10947
|
+
members: args.members,
|
|
10948
|
+
perDirectorSignals: args.perDirectorSignals,
|
|
10949
|
+
language: args.language,
|
|
10950
|
+
supplement: args.supplement,
|
|
10951
|
+
fallbackSource,
|
|
10952
|
+
fallbackFooterTag
|
|
10953
|
+
});
|
|
10954
|
+
emitStage(args.roomId, args.briefId, "write", "active", void 0, void 0, { lo: 8, hi: 24 });
|
|
10955
|
+
for (const modelV of stageFlagshipList()) {
|
|
10956
|
+
if (!isModelV(modelV)) continue;
|
|
10957
|
+
for (let attempt = 0; attempt < STAGE_2_RETRIES; attempt++) {
|
|
10958
|
+
try {
|
|
10959
|
+
let buf = "";
|
|
10960
|
+
let totalTokens = 0;
|
|
10961
|
+
for await (const chunk of callLLMStream({
|
|
10962
|
+
modelV,
|
|
10963
|
+
messages,
|
|
10964
|
+
temperature: STAGE_2_TEMPERATURES[attempt] ?? 0.6,
|
|
10965
|
+
maxTokens: 8e3,
|
|
10966
|
+
signal: args.signal
|
|
10967
|
+
})) {
|
|
10968
|
+
if (chunk.type === "text") {
|
|
10969
|
+
buf += chunk.delta;
|
|
10970
|
+
roomBus.emit(args.roomId, {
|
|
10971
|
+
type: "config-event",
|
|
10972
|
+
kind: "brief-token",
|
|
10973
|
+
payload: { briefId: args.briefId, delta: chunk.delta },
|
|
10974
|
+
createdAt: Date.now()
|
|
10975
|
+
});
|
|
10976
|
+
} else if (chunk.type === "usage") {
|
|
10977
|
+
totalTokens = chunk.totalTokens;
|
|
10978
|
+
} else if (chunk.type === "error") {
|
|
10979
|
+
throw new Error(chunk.message);
|
|
10980
|
+
}
|
|
10981
|
+
}
|
|
10982
|
+
if (totalTokens > 0) billChair(args.chairId, { totalTokens });
|
|
10983
|
+
const bento = parseBento(buf, args.room.subject, fallbackSource, fallbackFooterTag);
|
|
10984
|
+
if (bento) {
|
|
10985
|
+
if (attempt > 0) {
|
|
10986
|
+
process.stderr.write(
|
|
10987
|
+
`[brief.${args.mode}] ${modelV} succeeded on retry ${attempt + 1}
|
|
10988
|
+
`
|
|
10989
|
+
);
|
|
10990
|
+
}
|
|
10991
|
+
updateBriefBodyJson(args.briefId, bento, bento.title);
|
|
10992
|
+
emitStage(args.roomId, args.briefId, "write", "done");
|
|
10993
|
+
roomBus.emit(args.roomId, {
|
|
10994
|
+
type: "config-event",
|
|
10995
|
+
kind: "brief-final",
|
|
10996
|
+
payload: { briefId: args.briefId, title: bento.title, modelV },
|
|
10997
|
+
createdAt: Date.now()
|
|
10998
|
+
});
|
|
10999
|
+
return true;
|
|
11000
|
+
}
|
|
11001
|
+
process.stderr.write(
|
|
11002
|
+
`[brief.${args.mode}] ${modelV} attempt ${attempt + 1}/${STAGE_2_RETRIES} produced unparseable bento
|
|
11003
|
+
`
|
|
11004
|
+
);
|
|
11005
|
+
} catch (e) {
|
|
11006
|
+
if (args.signal?.aborted) throw e;
|
|
11007
|
+
process.stderr.write(
|
|
11008
|
+
`[brief.${args.mode}] ${modelV} attempt ${attempt + 1}/${STAGE_2_RETRIES} failed: ${e instanceof Error ? e.message : String(e)}
|
|
11009
|
+
`
|
|
11010
|
+
);
|
|
11011
|
+
}
|
|
11012
|
+
}
|
|
11013
|
+
}
|
|
11014
|
+
return false;
|
|
11015
|
+
}
|
|
9678
11016
|
function synthesizeDirectorPerspectivesFallback(perDirectorSignals, members) {
|
|
9679
11017
|
const memberById = new Map(members.map((m) => [m.id, m]));
|
|
9680
11018
|
const VALID_LENSES = /* @__PURE__ */ new Set([
|
|
@@ -9869,10 +11207,17 @@ function buildMethodologyFooter(args) {
|
|
|
9869
11207
|
|
|
9870
11208
|
// src/routes/briefs.ts
|
|
9871
11209
|
init_paths();
|
|
11210
|
+
function briefHasBody(b) {
|
|
11211
|
+
if (b.mode === "magazine" || b.mode === "newspaper" || b.mode === "ppt") {
|
|
11212
|
+
const j = b.bodyJson;
|
|
11213
|
+
return !!(j && typeof j === "object" && typeof j.title === "string" && j.title.length > 0);
|
|
11214
|
+
}
|
|
11215
|
+
return !!(b.bodyMd && b.bodyMd.trim().length > 0);
|
|
11216
|
+
}
|
|
9872
11217
|
function briefsRouter() {
|
|
9873
11218
|
const r = new Hono3();
|
|
9874
11219
|
r.get("/", (c) => {
|
|
9875
|
-
const briefs = listAllBriefs().filter((b) => !isBriefGenerating(b.id) && b
|
|
11220
|
+
const briefs = listAllBriefs().filter((b) => !isBriefGenerating(b.id) && briefHasBody(b));
|
|
9876
11221
|
return c.json({ briefs });
|
|
9877
11222
|
});
|
|
9878
11223
|
r.get("/count", (c) => {
|
|
@@ -9887,7 +11232,7 @@ function briefsRouter() {
|
|
|
9887
11232
|
const id = c.req.param("id");
|
|
9888
11233
|
const b = getBrief(id);
|
|
9889
11234
|
if (!b) return c.json({ error: "not found" }, 404);
|
|
9890
|
-
const hasBody =
|
|
11235
|
+
const hasBody = briefHasBody(b);
|
|
9891
11236
|
const generating = isBriefGenerating(id);
|
|
9892
11237
|
const completed = hasBody && !generating;
|
|
9893
11238
|
const state = generating ? getBriefGenerationState(id) : null;
|
|
@@ -10877,13 +12222,14 @@ function renderLongTermMemoryBlock(agentId, userName) {
|
|
|
10877
12222
|
const memories = memoriesForContext(agentId);
|
|
10878
12223
|
if (memories.length === 0) return "";
|
|
10879
12224
|
const lines = memories.map((m) => {
|
|
10880
|
-
const
|
|
10881
|
-
return ` \xB7 [${m.kind}
|
|
12225
|
+
const tag = m.pinned ? "pinned" : m.tier === "long" ? "stable" : "recent";
|
|
12226
|
+
return ` \xB7 [${tag}] [${m.kind}] ${m.content}`;
|
|
10882
12227
|
});
|
|
12228
|
+
bumpUsage(memories.map((m) => m.id));
|
|
10883
12229
|
return [
|
|
10884
12230
|
"",
|
|
10885
12231
|
`\u2500\u2500\u2500 WHAT YOU REMEMBER ABOUT ${userName} (cross-room, your own observations) \u2500\u2500\u2500`,
|
|
10886
|
-
`These are notes you've accumulated across previous rooms with this user \u2014 your lens, not other directors'. Treat them as priors, not facts. If something contradicts the current room, name it explicitly.`,
|
|
12232
|
+
`These are notes you've accumulated across previous rooms with this user \u2014 your lens, not other directors'. Treat them as priors, not facts. \`[stable]\` items have shown up across multiple rooms and are likely durable; \`[recent]\` items are still provisional. If something contradicts the current room, name it explicitly.`,
|
|
10887
12233
|
...lines,
|
|
10888
12234
|
""
|
|
10889
12235
|
].join("\n");
|
|
@@ -13016,6 +14362,15 @@ async function extractMemoriesAfterAdjourn(roomId) {
|
|
|
13016
14362
|
}
|
|
13017
14363
|
})
|
|
13018
14364
|
);
|
|
14365
|
+
for (const agent of agents) {
|
|
14366
|
+
if (!bumpAdjournCounter(agent.id, agent.roleKind)) continue;
|
|
14367
|
+
runDreamCycle(agent.id).catch((e) => {
|
|
14368
|
+
process.stderr.write(
|
|
14369
|
+
`[dream] ${agent.name} cycle failed: ${e instanceof Error ? e.message : String(e)}
|
|
14370
|
+
`
|
|
14371
|
+
);
|
|
14372
|
+
});
|
|
14373
|
+
}
|
|
13019
14374
|
}
|
|
13020
14375
|
async function runExtractionForAgent(agent, transcript, userName) {
|
|
13021
14376
|
const system = [
|
|
@@ -14147,13 +15502,13 @@ function resolvePickerModel() {
|
|
|
14147
15502
|
var TARGET_CAST_SIZE = 3;
|
|
14148
15503
|
var LENS_AXES = ["dissent", "rigor", "empathy", "pattern_recall"];
|
|
14149
15504
|
var LENS_THRESHOLD = 7;
|
|
14150
|
-
function
|
|
15505
|
+
function clipString2(s, max) {
|
|
14151
15506
|
if (s.length <= max) return s;
|
|
14152
15507
|
return s.slice(0, max).trim();
|
|
14153
15508
|
}
|
|
14154
15509
|
function describeDirector(a, recentCount) {
|
|
14155
15510
|
const ability = a.ability ? Object.entries(a.ability).filter(([, v]) => typeof v === "number" && v >= LENS_THRESHOLD).map(([k, v]) => `${k}:${v}`).join(",") : "";
|
|
14156
|
-
const bio =
|
|
15511
|
+
const bio = clipString2(a.bio || "", 140).replace(/\s+/g, " ");
|
|
14157
15512
|
const tag = (a.roleTag || "director").toLowerCase();
|
|
14158
15513
|
const recencyTag = recentCount > 0 ? ` \xB7 [seen ${recentCount}/5 recent rooms]` : ` \xB7 [unseen recently]`;
|
|
14159
15514
|
return `- ${a.handle} \xB7 ${a.name} \xB7 ${tag} \xB7 "${bio}"${ability ? ` \xB7 strong on { ${ability} }` : ""}${recencyTag}`;
|
|
@@ -14359,7 +15714,7 @@ async function pickDirectors(opts) {
|
|
|
14359
15714
|
const agent = byHandle.get(handle);
|
|
14360
15715
|
if (!agent) continue;
|
|
14361
15716
|
if (llmPicks.find((x) => x.agent.id === agent.id)) continue;
|
|
14362
|
-
const reason = typeof p.reason === "string" ?
|
|
15717
|
+
const reason = typeof p.reason === "string" ? clipString2(p.reason.trim(), 80) : "";
|
|
14363
15718
|
llmPicks.push({ agent, reason });
|
|
14364
15719
|
if (llmPicks.length >= TARGET_CAST_SIZE) break;
|
|
14365
15720
|
}
|
|
@@ -14374,7 +15729,7 @@ async function pickDirectors(opts) {
|
|
|
14374
15729
|
}
|
|
14375
15730
|
const reasonsByAgent = new Map(llmPicks.map((p) => [p.agent.id, p.reason]));
|
|
14376
15731
|
const adjusted = enforceDiversity(llmPicks.map((p) => p.agent), candidates);
|
|
14377
|
-
const rationale = typeof parsed.rationale === "string" ?
|
|
15732
|
+
const rationale = typeof parsed.rationale === "string" ? clipString2(parsed.rationale.trim(), 120) : "covers complementary lenses";
|
|
14378
15733
|
return {
|
|
14379
15734
|
picks: adjusted.map((a) => ({
|
|
14380
15735
|
agentId: a.id,
|
|
@@ -15092,6 +16447,7 @@ function roomsRouter() {
|
|
|
15092
16447
|
const explicit = typeof b.style === "string" && b.style ? b.style : null;
|
|
15093
16448
|
const fromRoom = room.briefStyle && room.briefStyle !== "auto" ? room.briefStyle : null;
|
|
15094
16449
|
const style = explicit || fromRoom || "mckinsey";
|
|
16450
|
+
const mode = b.mode === "magazine" || b.mode === "newspaper" || b.mode === "ppt" ? b.mode : "research-note";
|
|
15095
16451
|
abortRoom(id);
|
|
15096
16452
|
const adjournedAt = Date.now();
|
|
15097
16453
|
setRoomStatus(id, "adjourned", { adjournedAt });
|
|
@@ -15123,7 +16479,7 @@ function roomsRouter() {
|
|
|
15123
16479
|
}
|
|
15124
16480
|
let briefId = null;
|
|
15125
16481
|
try {
|
|
15126
|
-
const result = await generateBrief({ roomId: id, style });
|
|
16482
|
+
const result = await generateBrief({ roomId: id, style, mode });
|
|
15127
16483
|
briefId = result.briefId;
|
|
15128
16484
|
} catch (e) {
|
|
15129
16485
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -15172,11 +16528,13 @@ function roomsRouter() {
|
|
|
15172
16528
|
const explicit = typeof b.style === "string" && b.style ? b.style : null;
|
|
15173
16529
|
const fromRoom = room.briefStyle && room.briefStyle !== "auto" ? room.briefStyle : null;
|
|
15174
16530
|
const style = explicit || fromRoom || "mckinsey";
|
|
16531
|
+
const mode = b.mode === "magazine" || b.mode === "newspaper" || b.mode === "ppt" ? b.mode : "research-note";
|
|
15175
16532
|
try {
|
|
15176
16533
|
const result = await generateBrief({
|
|
15177
16534
|
roomId: id,
|
|
15178
16535
|
style,
|
|
15179
|
-
supplement: supplement || void 0
|
|
16536
|
+
supplement: supplement || void 0,
|
|
16537
|
+
mode
|
|
15180
16538
|
});
|
|
15181
16539
|
return c.json({ briefId: result.briefId, status: "generating" });
|
|
15182
16540
|
} catch (e) {
|
|
@@ -15337,7 +16695,7 @@ function usageRouter() {
|
|
|
15337
16695
|
init_paths();
|
|
15338
16696
|
|
|
15339
16697
|
// src/version.ts
|
|
15340
|
-
var VERSION = "0.1.
|
|
16698
|
+
var VERSION = "0.1.9";
|
|
15341
16699
|
|
|
15342
16700
|
// src/server.ts
|
|
15343
16701
|
function createApp() {
|
|
@@ -15469,6 +16827,25 @@ async function main() {
|
|
|
15469
16827
|
process.stderr.write(`[boot] clarify recovery failed: ${e instanceof Error ? e.message : String(e)}
|
|
15470
16828
|
`);
|
|
15471
16829
|
}
|
|
16830
|
+
void (async () => {
|
|
16831
|
+
try {
|
|
16832
|
+
const agents = listAllAgents();
|
|
16833
|
+
let triggered = 0;
|
|
16834
|
+
for (const agent of agents) {
|
|
16835
|
+
if (countMemoriesForAgent(agent.id) > bootCeilingFor(agent.roleKind)) {
|
|
16836
|
+
await runDreamCycle(agent.id);
|
|
16837
|
+
triggered += 1;
|
|
16838
|
+
}
|
|
16839
|
+
}
|
|
16840
|
+
if (triggered > 0) {
|
|
16841
|
+
process.stderr.write(`[boot] dream sweep triggered for ${triggered} agent(s) over ceiling
|
|
16842
|
+
`);
|
|
16843
|
+
}
|
|
16844
|
+
} catch (e) {
|
|
16845
|
+
process.stderr.write(`[boot] dream sweep failed: ${e instanceof Error ? e.message : String(e)}
|
|
16846
|
+
`);
|
|
16847
|
+
}
|
|
16848
|
+
})();
|
|
15472
16849
|
const portArg = opts.port ? Number.parseInt(opts.port, 10) : void 0;
|
|
15473
16850
|
if (portArg !== void 0 && (Number.isNaN(portArg) || portArg < 1 || portArg > 65535)) {
|
|
15474
16851
|
console.error(`Invalid --port: ${opts.port}`);
|