privateboard 0.1.8 → 0.1.10
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 +879 -180
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +2 -1
- package/public/agent-profile.css +525 -0
- package/public/agent-profile.js +278 -1
- package/public/app.js +279 -120
- package/public/index.html +312 -164
- package/public/magazine.html +1257 -838
- package/public/newspaper.html +1389 -1267
- package/public/ppt.html +2623 -0
- package/public/room-settings.css +40 -4
- package/public/room-settings.js +44 -2
- package/public/user-settings.css +23 -21
- package/public/user-settings.js +1 -4
- package/public/bento.html +0 -1396
package/dist/cli.js
CHANGED
|
@@ -384,6 +384,69 @@ var init_brief_mode = __esm({
|
|
|
384
384
|
}
|
|
385
385
|
});
|
|
386
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
|
+
|
|
387
450
|
// src/storage/db.ts
|
|
388
451
|
var db_exports = {};
|
|
389
452
|
__export(db_exports, {
|
|
@@ -466,6 +529,8 @@ var init_db = __esm({
|
|
|
466
529
|
init_brief_assets();
|
|
467
530
|
init_usage_daily();
|
|
468
531
|
init_brief_mode();
|
|
532
|
+
init_memory_metabolism();
|
|
533
|
+
init_memory_metabolism_p2();
|
|
469
534
|
MIGRATIONS = [
|
|
470
535
|
{ name: "001_init.sql", sql: init_default },
|
|
471
536
|
{ name: "002_default_opus.sql", sql: default_opus_default },
|
|
@@ -491,7 +556,9 @@ var init_db = __esm({
|
|
|
491
556
|
{ name: "022_intensity_brutal_to_terse.sql", sql: intensity_brutal_to_terse_default },
|
|
492
557
|
{ name: "023_brief_assets.sql", sql: brief_assets_default },
|
|
493
558
|
{ name: "024_usage_daily.sql", sql: usage_daily_default },
|
|
494
|
-
{ name: "025_brief_mode.sql", sql: brief_mode_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 }
|
|
495
562
|
];
|
|
496
563
|
_db = null;
|
|
497
564
|
}
|
|
@@ -830,6 +897,10 @@ function updateAgent(id, patch) {
|
|
|
830
897
|
const json = patch.ability && Object.keys(patch.ability).length > 0 ? JSON.stringify(patch.ability) : null;
|
|
831
898
|
values.push(json);
|
|
832
899
|
}
|
|
900
|
+
if (typeof patch.isPinned === "boolean") {
|
|
901
|
+
fields.push("is_pinned = ?");
|
|
902
|
+
values.push(patch.isPinned ? 1 : 0);
|
|
903
|
+
}
|
|
833
904
|
if (fields.length === 0) return getAgent(id);
|
|
834
905
|
fields.push("updated_at = ?");
|
|
835
906
|
values.push(Date.now());
|
|
@@ -1774,12 +1845,22 @@ function newId(len = 12) {
|
|
|
1774
1845
|
}
|
|
1775
1846
|
|
|
1776
1847
|
// src/storage/memories.ts
|
|
1777
|
-
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";
|
|
1778
1849
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["fact", "observation", "preference", "goal"]);
|
|
1779
1850
|
var ALLOWED_SOURCES = /* @__PURE__ */ new Set(["extracted", "user_added", "user_pinned"]);
|
|
1851
|
+
var ALLOWED_TIERS = /* @__PURE__ */ new Set(["short", "long"]);
|
|
1780
1852
|
function mapRow2(row) {
|
|
1781
1853
|
const kind = ALLOWED_KINDS.has(row.kind) ? row.kind : "fact";
|
|
1782
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
|
+
}
|
|
1783
1864
|
return {
|
|
1784
1865
|
id: row.id,
|
|
1785
1866
|
agentId: row.agent_id,
|
|
@@ -1790,13 +1871,20 @@ function mapRow2(row) {
|
|
|
1790
1871
|
confidence: row.confidence,
|
|
1791
1872
|
pinned: row.pinned === 1,
|
|
1792
1873
|
createdAt: row.created_at,
|
|
1793
|
-
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
|
|
1794
1881
|
};
|
|
1795
1882
|
}
|
|
1796
|
-
function listMemoriesForAgent(agentId) {
|
|
1883
|
+
function listMemoriesForAgent(agentId, opts = {}) {
|
|
1884
|
+
const where = opts.includeSuperseded ? "WHERE agent_id = ?" : "WHERE agent_id = ? AND superseded_by IS NULL";
|
|
1797
1885
|
const rows = getDb().prepare(
|
|
1798
1886
|
`SELECT ${SELECT_COLS2} FROM agent_memories
|
|
1799
|
-
|
|
1887
|
+
${where}
|
|
1800
1888
|
ORDER BY pinned DESC, created_at DESC`
|
|
1801
1889
|
).all(agentId);
|
|
1802
1890
|
return rows.map(mapRow2);
|
|
@@ -1804,8 +1892,52 @@ function listMemoriesForAgent(agentId) {
|
|
|
1804
1892
|
function memoriesForContext(agentId, recentCap = 5) {
|
|
1805
1893
|
const all = listMemoriesForAgent(agentId);
|
|
1806
1894
|
const pinned = all.filter((m) => m.pinned);
|
|
1807
|
-
const
|
|
1808
|
-
|
|
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;
|
|
1809
1941
|
}
|
|
1810
1942
|
function insertMemory(input) {
|
|
1811
1943
|
const db = getDb();
|
|
@@ -1857,6 +1989,109 @@ function deleteMemory(id) {
|
|
|
1857
1989
|
function isMemoryKind(v) {
|
|
1858
1990
|
return ALLOWED_KINDS.has(v);
|
|
1859
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
|
+
}
|
|
1860
2095
|
|
|
1861
2096
|
// src/storage/skills.ts
|
|
1862
2097
|
init_db();
|
|
@@ -3687,6 +3922,330 @@ function formatSearchResults(query, results) {
|
|
|
3687
3922
|
return lines.join("\n");
|
|
3688
3923
|
}
|
|
3689
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
|
+
|
|
3690
4249
|
// src/routes/agents.ts
|
|
3691
4250
|
function agentSpecModelCandidates() {
|
|
3692
4251
|
const out = [];
|
|
@@ -4035,6 +4594,9 @@ function agentsRouter() {
|
|
|
4035
4594
|
if (typeof b.webSearchEnabled === "boolean") {
|
|
4036
4595
|
patch.webSearchEnabled = b.webSearchEnabled;
|
|
4037
4596
|
}
|
|
4597
|
+
if (typeof b.isPinned === "boolean") {
|
|
4598
|
+
patch.isPinned = b.isPinned;
|
|
4599
|
+
}
|
|
4038
4600
|
const updated = updateAgent(id, patch);
|
|
4039
4601
|
return c.json(updated);
|
|
4040
4602
|
});
|
|
@@ -4125,6 +4687,24 @@ function agentsRouter() {
|
|
|
4125
4687
|
if (!ok) return c.json({ error: "not found" }, 404);
|
|
4126
4688
|
return c.json({ ok: true });
|
|
4127
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
|
+
});
|
|
4128
4708
|
r.get("/:id/skills", (c) => {
|
|
4129
4709
|
const id = c.req.param("id");
|
|
4130
4710
|
const agent = getAgent(id);
|
|
@@ -5423,150 +6003,6 @@ ${lines}`;
|
|
|
5423
6003
|
}
|
|
5424
6004
|
];
|
|
5425
6005
|
}
|
|
5426
|
-
var BENTO_SYSTEM = [
|
|
5427
|
-
`You are the chair of a boardroom session. You produce a SINGLE-PAGE INFOGRAPHIC report \u2014 a structured "bento box" that compresses the room's discussion into a one-screen visual brief. Not a memo. Not a research note. A poster.`,
|
|
5428
|
-
"",
|
|
5429
|
-
"## What a bento is for",
|
|
5430
|
-
"",
|
|
5431
|
-
"Bento is for the moment AFTER the discussion when the user wants to forward the takeaway to someone in 60 seconds. Not the analysis itself \xB7 the answer. Not the room's debate \xB7 the conclusion the room reached. Not all the evidence \xB7 the 3 most load-bearing claims with their numerics.",
|
|
5432
|
-
"",
|
|
5433
|
-
"Lossy is feature, not bug. If you can't compress to 3 milestones + a one-line conclusion, you didn't pick the load-bearing pieces. Compression is the work.",
|
|
5434
|
-
"",
|
|
5435
|
-
"## The 8 slots you fill (output is JSON only)",
|
|
5436
|
-
"",
|
|
5437
|
-
'1. **title** \xB7 the takeaway in claim form \xB7 serif headline \xB7 \u2264 110 chars \xB7 ONE sentence that names what the room concluded. Quantified is stronger ("X cuts cost 10\xD7" beats "X is meaningful").',
|
|
5438
|
-
"",
|
|
5439
|
-
"2. **kicker** \xB7 1 italic sentence \xB7 \u2264 200 chars \xB7 the angle / what's new about this conclusion. Reads like a magazine deck under the headline.",
|
|
5440
|
-
"",
|
|
5441
|
-
'3. **source** \xB7 \u2264 80 chars \xB7 attribution / context. Format: "From {chair name} \xB7 {date or horizon}" or "{room subject short} \xB7 {date}". Mono small caps register; auto-filled if you skip.',
|
|
5442
|
-
"",
|
|
5443
|
-
"4. **milestones** \xB7 EXACTLY 3 cards in the LEFT timeline. Each card has:",
|
|
5444
|
-
' \xB7 `period` \xB7 time / phase tag \xB7 \u2264 24 chars \xB7 "2025H2" / "Q1 2026" / "Phase 2" / "Top finding" / "Step 1". Choose the lens (chronological, ranked, or sequential) that fits this room.',
|
|
5445
|
-
" \xB7 `title` \xB7 \u2264 60 chars \xB7 the milestone's name in claim form.",
|
|
5446
|
-
" \xB7 `body` \xB7 2-3 sentences \xB7 \u2264 220 chars \xB7 what happened / will happen / why it matters.",
|
|
5447
|
-
' \xB7 `callout` \xB7 \u2264 12 chars \xB7 the metric / multiplier / count that anchors this card visually. Examples: "-10\xD7", "2000\u4E07\u9897", "$120M ARR", "T+90". Empty string when no clean numeric exists for this milestone.',
|
|
5448
|
-
" \xB7 `tags` \xB7 0-4 short chips \xB7 \u2264 16 chars each \xB7 entity names, owners, domains. Render as small rounded chips beside the body.",
|
|
5449
|
-
"",
|
|
5450
|
-
" Pick the 3 most load-bearing pieces of the discussion. NOT the 3 most-mentioned. The 3 that, taken together, produce the takeaway from \xA71.",
|
|
5451
|
-
"",
|
|
5452
|
-
"5. **rankedBars** \xB7 OPTIONAL \xB7 top-right card \xB7 3-5 ranked entries with normalised ratio bars. Pick this slot when the room produced quantitative comparisons (e.g. competitor TAM, model latencies, milestone costs). Each entry: `label` (\u2264 40), `value` (\u2264 20, the displayed number), `ratio` (0-1 normalised for bar width \u2014 divide by the largest entry). Set to `null` when the room had no real ranked-numeric material.",
|
|
5453
|
-
"",
|
|
5454
|
-
'6. **verification** \xB7 OPTIONAL \xB7 mid-right card \xB7 3-5 single-sentence bullets \xB7 \u2264 140 chars each \xB7 the validation signals that, if observed, would confirm the takeaway. Maps from convergence + leading-indicator material. The card title is your call (default "What we\'d verify" / "\u9A8C\u8BC1\u7EBF\u7D22"). Set to `null` when the room raised no clean signals to watch.',
|
|
5455
|
-
"",
|
|
5456
|
-
'7. **talkingPoints** \xB7 ALWAYS rendered \xB7 bottom-right card \xB7 3-5 elevator-pitch sentences \xB7 \u2264 120 chars each \xB7 what a reader could literally say to brief a colleague verbally. Imperative, declarative, no hedging. Maps from recommendations + bottom line, collapsed to single declarative sentences. Default title "How to say this" / "\u53E3\u64AD\u63D0\u7EB2".',
|
|
5457
|
-
"",
|
|
5458
|
-
"8. **conclusion** \xB7 bottom band \xB7 \u2264 100 chars \xB7 ONE sentence \xB7 the takeaway compressed further than the title. The reader walks away with this single line.",
|
|
5459
|
-
"",
|
|
5460
|
-
'Plus optional **flow** \xB7 2-4 short nodes joined by arrows at render time \xB7 for transformations the room argued ("before \u2192 after" / "10\xD7 \u2192 10\xD7" / "weak \u2192 defensible"). Set to `null` when no clean transformation arc exists.',
|
|
5461
|
-
"",
|
|
5462
|
-
"Plus auto **footerTag** \xB7 \u2264 80 chars \xB7 short caption mono caps \xB7 room subject + horizon. Auto-filled if you skip.",
|
|
5463
|
-
"",
|
|
5464
|
-
"## Routing the SIGNALS block into bento slots",
|
|
5465
|
-
"",
|
|
5466
|
-
"The SIGNALS block carries each director's extracted material with kind prefixes (`[claim]`, `[evidence\xB7data]`, `[risk]`, etc.). Route them into bento slots:",
|
|
5467
|
-
" \xB7 **milestones** \u2190 the 3 strongest `[claim]` + `[evidence\xB7data]` pairs \xB7 pair each claim with its supporting datapoint when one exists; that becomes the card's `body` + `callout`.",
|
|
5468
|
-
" \xB7 **rankedBars** \u2190 `[evidence\xB7data]` entries that are numeric AND comparable (the room mentioned multiple options / competitors / sizes / dates as ranked numerics).",
|
|
5469
|
-
' \xB7 **verification** \u2190 `[evidence\xB7data]` + `[claim]` material that READS as something to monitor ("if X stays above Y, the call holds").',
|
|
5470
|
-
" \xB7 **talkingPoints** \u2190 `[action]` + `[claim\xB7confidence:high]` material distilled to imperative single-sentence form.",
|
|
5471
|
-
" \xB7 **conclusion** \u2190 compressed restatement of the room's anchor (Bottom Line / Thesis equivalent).",
|
|
5472
|
-
" \xB7 **flow** \u2190 when the room argued a transformation (X becomes Y) or a multi-step path, distill it to 2-4 nodes.",
|
|
5473
|
-
"",
|
|
5474
|
-
"## Output format",
|
|
5475
|
-
"",
|
|
5476
|
-
"Strict JSON inside a fenced ```json code block. No prose outside the block. The shape is fixed:",
|
|
5477
|
-
"",
|
|
5478
|
-
"```json",
|
|
5479
|
-
"{",
|
|
5480
|
-
' "title": "Sentence-form takeaway with a quantified element when one fits.",',
|
|
5481
|
-
' "kicker": "1-sentence italic deck explaining the angle.",',
|
|
5482
|
-
' "source": "From {chair name} \xB7 {date or horizon}",',
|
|
5483
|
-
' "milestones": [',
|
|
5484
|
-
' { "period": "2025H2", "title": "Milestone name", "body": "2-3 sentences.", "callout": "-10\xD7", "tags": ["AWS", "GCP"] },',
|
|
5485
|
-
' { "period": "Q1 2026", "title": "...", "body": "...", "callout": "...", "tags": [] },',
|
|
5486
|
-
' { "period": "2026H2", "title": "...", "body": "...", "callout": "...", "tags": [] }',
|
|
5487
|
-
" ],",
|
|
5488
|
-
' "rankedBars": {',
|
|
5489
|
-
' "title": "By the numbers",',
|
|
5490
|
-
' "entries": [',
|
|
5491
|
-
' { "label": "Hopper", "value": "1.0\xD7", "ratio": 1.0 },',
|
|
5492
|
-
' { "label": "Blackwell", "value": "0.1\xD7", "ratio": 0.1 },',
|
|
5493
|
-
' { "label": "Rubin", "value": "0.01\xD7", "ratio": 0.01 }',
|
|
5494
|
-
" ]",
|
|
5495
|
-
" },",
|
|
5496
|
-
' "verification": {',
|
|
5497
|
-
` "title": "What we'd verify",`,
|
|
5498
|
-
' "bullets": [',
|
|
5499
|
-
' "Single sentence verification signal #1.",',
|
|
5500
|
-
' "Single sentence verification signal #2.",',
|
|
5501
|
-
' "Single sentence verification signal #3."',
|
|
5502
|
-
" ]",
|
|
5503
|
-
" },",
|
|
5504
|
-
' "talkingPoints": {',
|
|
5505
|
-
' "title": "How to say this",',
|
|
5506
|
-
' "bullets": [',
|
|
5507
|
-
' "First elevator-pitch sentence.",',
|
|
5508
|
-
' "Second elevator-pitch sentence.",',
|
|
5509
|
-
' "Third elevator-pitch sentence."',
|
|
5510
|
-
" ]",
|
|
5511
|
-
" },",
|
|
5512
|
-
' "conclusion": "One-line takeaway \xB7 \u2264 100 chars.",',
|
|
5513
|
-
' "flow": { "nodes": ["Hopper", "Blackwell", "Rubin"], "caption": "Two-stage cost step-down" },',
|
|
5514
|
-
' "footerTag": "Q4 update \xB7 2025H2 \u2192 2026H2"',
|
|
5515
|
-
"}",
|
|
5516
|
-
"```",
|
|
5517
|
-
"",
|
|
5518
|
-
"Constraints:",
|
|
5519
|
-
'\xB7 Title MUST be claim-style (state the takeaway, not the topic). "Three commitments that change the trajectory" not "Analysis of strategic options".',
|
|
5520
|
-
"\xB7 Milestones MUST be 3. Pad with the most-mentioned claim if the room only surfaced 2 strong points; trim to the 3 most load-bearing if the room surfaced more.",
|
|
5521
|
-
'\xB7 `callout` field carries ONE numeric or unit \xB7 no English plus number combinations ("$120M ARR" OK; "makes $120M" not OK).',
|
|
5522
|
-
"\xB7 `talkingPoints` is mandatory \xB7 if the room had no recommendations, distil the bottom-line claim into 3 ways a colleague could quote it.",
|
|
5523
|
-
"\xB7 No markdown formatting inside string fields. No bullet characters. No headings. Plain prose only \u2014 the renderer adds visual structure."
|
|
5524
|
-
].join("\n");
|
|
5525
|
-
function buildBentoMessages(opts) {
|
|
5526
|
-
const { room, members, perDirectorSignals, language } = opts;
|
|
5527
|
-
const memberList = members.map((a) => `${a.id} \xB7 ${a.name} (${a.handle}) \u2014 ${a.roleTag}`).join("\n \xB7 ");
|
|
5528
|
-
const signalsBlock = perDirectorSignals.map((d) => {
|
|
5529
|
-
if (!d.signals.length) return `[${d.directorId}] ${d.directorName} \u2014 (no signals)`;
|
|
5530
|
-
const lines = d.signals.map((s, i) => ` \xB7 ${d.directorId}#${i} [${s.lens}] ${s.text}`).join("\n");
|
|
5531
|
-
return `[${d.directorId}] ${d.directorName}
|
|
5532
|
-
${lines}`;
|
|
5533
|
-
}).join("\n\n");
|
|
5534
|
-
const supplementBlock = opts.supplement && opts.supplement.trim() ? [
|
|
5535
|
-
``,
|
|
5536
|
-
`\u2500\u2500\u2500 SUPPLEMENTARY PERSPECTIVE FROM USER \u2500\u2500\u2500`,
|
|
5537
|
-
``,
|
|
5538
|
-
`The user has asked you to additionally consider this angle when building the bento. Surface it in the most fitting slot (most often as one of the milestones, occasionally as a verification bullet or a talking point).`,
|
|
5539
|
-
``,
|
|
5540
|
-
opts.supplement.trim(),
|
|
5541
|
-
``,
|
|
5542
|
-
`\u2500\u2500\u2500 END SUPPLEMENT \u2500\u2500\u2500`
|
|
5543
|
-
].join("\n") : "";
|
|
5544
|
-
return [
|
|
5545
|
-
{
|
|
5546
|
-
role: "system",
|
|
5547
|
-
content: [BENTO_SYSTEM, "", languageInstruction(language)].join("\n")
|
|
5548
|
-
},
|
|
5549
|
-
{
|
|
5550
|
-
role: "user",
|
|
5551
|
-
content: [
|
|
5552
|
-
`ROOM #${room.number} \xB7 ${room.name}`,
|
|
5553
|
-
`Subject: ${room.subject}`,
|
|
5554
|
-
``,
|
|
5555
|
-
`Directors:`,
|
|
5556
|
-
` \xB7 ${memberList}`,
|
|
5557
|
-
``,
|
|
5558
|
-
`\u2500\u2500\u2500 SIGNALS \u2500\u2500\u2500`,
|
|
5559
|
-
``,
|
|
5560
|
-
signalsBlock || "(no signals extracted)",
|
|
5561
|
-
``,
|
|
5562
|
-
`\u2500\u2500\u2500 END SIGNALS \u2500\u2500\u2500`,
|
|
5563
|
-
supplementBlock,
|
|
5564
|
-
``,
|
|
5565
|
-
`Produce the bento now. JSON only.`
|
|
5566
|
-
].join("\n")
|
|
5567
|
-
}
|
|
5568
|
-
];
|
|
5569
|
-
}
|
|
5570
6006
|
var MAGAZINE_SYSTEM = [
|
|
5571
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.',
|
|
5572
6008
|
"",
|
|
@@ -5618,7 +6054,7 @@ var MAGAZINE_SYSTEM = [
|
|
|
5618
6054
|
"",
|
|
5619
6055
|
"## Output format",
|
|
5620
6056
|
"",
|
|
5621
|
-
"Strict JSON inside a fenced ```json code block. No prose outside the block. The shape is fixed
|
|
6057
|
+
"Strict JSON inside a fenced ```json code block. No prose outside the block. The shape is fixed:",
|
|
5622
6058
|
"",
|
|
5623
6059
|
"```json",
|
|
5624
6060
|
"{",
|
|
@@ -5708,7 +6144,7 @@ ${lines}`;
|
|
|
5708
6144
|
];
|
|
5709
6145
|
}
|
|
5710
6146
|
var NEWSPAPER_SYSTEM = [
|
|
5711
|
-
"You are the chair of a boardroom session. You produce a
|
|
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.",
|
|
5712
6148
|
"",
|
|
5713
6149
|
"## Voice",
|
|
5714
6150
|
"",
|
|
@@ -5724,37 +6160,39 @@ var NEWSPAPER_SYSTEM = [
|
|
|
5724
6160
|
"",
|
|
5725
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.',
|
|
5726
6162
|
"",
|
|
5727
|
-
"4. **milestones** \xB7 EXACTLY 3 column-stories
|
|
6163
|
+
"4. **milestones** \xB7 EXACTLY 3 column-stories \xB7 they distribute across the 3 pages. Each card has:",
|
|
5728
6164
|
' \xB7 `period` \xB7 column section label \xB7 \u2264 24 chars \xB7 "TOP STORY" / "MARKETS" / "POLICY" / "OPS" / "OPINION". Section-banner register. ALL-CAPS-ABLE.',
|
|
5729
6165
|
" \xB7 `title` \xB7 column subheading \xB7 \u2264 60 chars \xB7 the column's hook. Question or claim form OK.",
|
|
5730
|
-
" \xB7 `body` \xB7 4-7 sentences \xB7 \u2264
|
|
5731
|
-
|
|
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.',
|
|
5732
6168
|
" \xB7 `tags` \xB7 empty array.",
|
|
5733
6169
|
"",
|
|
5734
|
-
|
|
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.",
|
|
5735
6171
|
"",
|
|
5736
|
-
|
|
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.",
|
|
5737
6173
|
"",
|
|
5738
|
-
|
|
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.',
|
|
5739
6175
|
"",
|
|
5740
|
-
|
|
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.",
|
|
5741
6177
|
"",
|
|
5742
|
-
"
|
|
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.",
|
|
5743
6181
|
"",
|
|
5744
6182
|
"Plus auto **footerTag** \xB7 \u2264 80 chars \xB7 masthead-style date caption \xB7 auto-filled if you skip.",
|
|
5745
6183
|
"",
|
|
5746
6184
|
"## Routing the SIGNALS block into newspaper slots",
|
|
5747
6185
|
"",
|
|
5748
|
-
" \xB7 **title** \u2190 the strongest claim \xB7 phrased as a front-page headline (declarative, claim-form).",
|
|
6186
|
+
" \xB7 **title** \u2190 the strongest claim \xB7 phrased as a front-page banner headline (declarative, claim-form).",
|
|
5749
6187
|
" \xB7 **kicker** \u2190 the supporting deck \xB7 \u2264 1 sentence \xB7 what's new about this conclusion.",
|
|
5750
|
-
" \xB7 **milestones** \u2190 the 3 most load-bearing
|
|
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.",
|
|
5751
6189
|
` \xB7 **verification** \u2190 the room's secondary findings \xB7 each phrased as "Heading: body." with a colon separator.`,
|
|
5752
|
-
|
|
5753
|
-
" \xB7 **conclusion** \u2190 the
|
|
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.",
|
|
5754
6192
|
"",
|
|
5755
6193
|
"## Output format",
|
|
5756
6194
|
"",
|
|
5757
|
-
"Strict JSON inside a fenced ```json code block. No prose outside the block. The shape is fixed
|
|
6195
|
+
"Strict JSON inside a fenced ```json code block. No prose outside the block. The shape is fixed:",
|
|
5758
6196
|
"",
|
|
5759
6197
|
"```json",
|
|
5760
6198
|
"{",
|
|
@@ -5841,6 +6279,172 @@ ${lines}`;
|
|
|
5841
6279
|
}
|
|
5842
6280
|
];
|
|
5843
6281
|
}
|
|
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;
|
|
6405
|
+
const memberList = members.map((a) => `${a.id} \xB7 ${a.name} (${a.handle}) \u2014 ${a.roleTag}`).join("\n \xB7 ");
|
|
6406
|
+
const signalsBlock = perDirectorSignals.map((d) => {
|
|
6407
|
+
if (!d.signals.length) return `[${d.directorId}] ${d.directorName} \u2014 (no signals)`;
|
|
6408
|
+
const lines = d.signals.map((s, i) => ` \xB7 ${d.directorId}#${i} [${s.lens}] ${s.text}`).join("\n");
|
|
6409
|
+
return `[${d.directorId}] ${d.directorName}
|
|
6410
|
+
${lines}`;
|
|
6411
|
+
}).join("\n\n");
|
|
6412
|
+
const supplementBlock = opts.supplement && opts.supplement.trim() ? [
|
|
6413
|
+
``,
|
|
6414
|
+
`\u2500\u2500\u2500 SUPPLEMENTARY PERSPECTIVE FROM USER \u2500\u2500\u2500`,
|
|
6415
|
+
``,
|
|
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).`,
|
|
6417
|
+
``,
|
|
6418
|
+
opts.supplement.trim(),
|
|
6419
|
+
``,
|
|
6420
|
+
`\u2500\u2500\u2500 END SUPPLEMENT \u2500\u2500\u2500`
|
|
6421
|
+
].join("\n") : "";
|
|
6422
|
+
return [
|
|
6423
|
+
{
|
|
6424
|
+
role: "system",
|
|
6425
|
+
content: [PPT_SYSTEM, "", languageInstruction(language)].join("\n")
|
|
6426
|
+
},
|
|
6427
|
+
{
|
|
6428
|
+
role: "user",
|
|
6429
|
+
content: [
|
|
6430
|
+
`ROOM #${room.number} \xB7 ${room.name}`,
|
|
6431
|
+
`Subject: ${room.subject}`,
|
|
6432
|
+
``,
|
|
6433
|
+
`Directors:`,
|
|
6434
|
+
` \xB7 ${memberList}`,
|
|
6435
|
+
``,
|
|
6436
|
+
`\u2500\u2500\u2500 SIGNALS \u2500\u2500\u2500`,
|
|
6437
|
+
``,
|
|
6438
|
+
signalsBlock || "(no signals extracted)",
|
|
6439
|
+
``,
|
|
6440
|
+
`\u2500\u2500\u2500 END SIGNALS \u2500\u2500\u2500`,
|
|
6441
|
+
supplementBlock,
|
|
6442
|
+
``,
|
|
6443
|
+
`Produce the slide deck now. JSON only.`
|
|
6444
|
+
].join("\n")
|
|
6445
|
+
}
|
|
6446
|
+
];
|
|
6447
|
+
}
|
|
5844
6448
|
var WRITE_SYSTEM = [
|
|
5845
6449
|
"You are the chair of a boardroom session. You have a structured scaffold. Write the final report in markdown \u2014 a McKinsey-grade research note that makes the multi-director thinking visible. Pyramid principle, MECE, action-oriented.",
|
|
5846
6450
|
"",
|
|
@@ -8167,6 +8771,7 @@ function parseBento(raw, fallbackTitle, fallbackSource, fallbackFooterTag) {
|
|
|
8167
8771
|
const conclusion = clipString(stringField(parsed.conclusion), 100);
|
|
8168
8772
|
const flow = parseBentoFlow(parsed.flow);
|
|
8169
8773
|
const footerTag = clipString(stringField(parsed.footerTag) || fallbackFooterTag, 80);
|
|
8774
|
+
const directorBlock = parsePptDirectorBlock(parsed.directorBlock);
|
|
8170
8775
|
return {
|
|
8171
8776
|
title,
|
|
8172
8777
|
kicker,
|
|
@@ -8177,9 +8782,70 @@ function parseBento(raw, fallbackTitle, fallbackSource, fallbackFooterTag) {
|
|
|
8177
8782
|
talkingPoints,
|
|
8178
8783
|
conclusion,
|
|
8179
8784
|
flow,
|
|
8180
|
-
footerTag
|
|
8785
|
+
footerTag,
|
|
8786
|
+
directorBlock
|
|
8181
8787
|
};
|
|
8182
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
|
+
}
|
|
8183
8849
|
function stringField(v) {
|
|
8184
8850
|
return typeof v === "string" ? v.trim() : "";
|
|
8185
8851
|
}
|
|
@@ -9309,7 +9975,11 @@ function mapRow7(row) {
|
|
|
9309
9975
|
subjectType: row.subject_type,
|
|
9310
9976
|
houseStyle: row.house_style || "boardroom-default",
|
|
9311
9977
|
assets: parseAssets(row.assets_json),
|
|
9312
|
-
|
|
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",
|
|
9313
9983
|
createdAt: row.created_at
|
|
9314
9984
|
};
|
|
9315
9985
|
}
|
|
@@ -9355,7 +10025,7 @@ function insertBrief(b) {
|
|
|
9355
10025
|
const composerRationale = b.composerRationale && b.composerRationale.trim() ? b.composerRationale.trim() : null;
|
|
9356
10026
|
const subjectType = b.subjectType && b.subjectType.trim() ? b.subjectType.trim() : null;
|
|
9357
10027
|
const houseStyle = b.houseStyle && b.houseStyle.trim() ? b.houseStyle.trim() : "boardroom-default";
|
|
9358
|
-
const mode = b.mode === "
|
|
10028
|
+
const mode = b.mode === "magazine" || b.mode === "newspaper" || b.mode === "ppt" ? b.mode : "research-note";
|
|
9359
10029
|
db.prepare(
|
|
9360
10030
|
`INSERT INTO briefs (${COLS2}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
9361
10031
|
).run(
|
|
@@ -9543,7 +10213,7 @@ function abortBriefGeneration(briefId) {
|
|
|
9543
10213
|
async function generateBrief(opts) {
|
|
9544
10214
|
const { roomId } = opts;
|
|
9545
10215
|
const style = opts.style ?? "mckinsey";
|
|
9546
|
-
const mode = opts.mode === "
|
|
10216
|
+
const mode = opts.mode === "magazine" || opts.mode === "newspaper" || opts.mode === "ppt" ? opts.mode : "research-note";
|
|
9547
10217
|
const room = getRoom(roomId);
|
|
9548
10218
|
if (!room) throw new Error(`room not found: ${roomId}`);
|
|
9549
10219
|
const memberRows = listRoomMembers(roomId);
|
|
@@ -9811,7 +10481,7 @@ async function runPipeline(args) {
|
|
|
9811
10481
|
`
|
|
9812
10482
|
);
|
|
9813
10483
|
}
|
|
9814
|
-
if (args.mode === "
|
|
10484
|
+
if (args.mode === "magazine" || args.mode === "newspaper" || args.mode === "ppt") {
|
|
9815
10485
|
const ok = await runBentoStage({
|
|
9816
10486
|
roomId,
|
|
9817
10487
|
briefId,
|
|
@@ -9826,7 +10496,7 @@ async function runPipeline(args) {
|
|
|
9826
10496
|
signal: args.signal
|
|
9827
10497
|
});
|
|
9828
10498
|
if (!ok) {
|
|
9829
|
-
const label = args.mode === "magazine" ? "Magazine" : args.mode === "
|
|
10499
|
+
const label = args.mode === "magazine" ? "Magazine" : args.mode === "ppt" ? "Slide deck" : "Newspaper";
|
|
9830
10500
|
pipelineError = `${label} writer couldn't structure this room (3 retries failed). Try regenerating, or shorten the conversation.`;
|
|
9831
10501
|
}
|
|
9832
10502
|
return;
|
|
@@ -10270,7 +10940,7 @@ async function runBentoStage(args) {
|
|
|
10270
10940
|
const fallbackSource = `From ${chairName} \xB7 ${today}`;
|
|
10271
10941
|
const subjectShort = (args.room.subject || "").slice(0, 60);
|
|
10272
10942
|
const fallbackFooterTag = subjectShort ? `${subjectShort} \xB7 ${today}` : today;
|
|
10273
|
-
const buildMessages = args.mode === "
|
|
10943
|
+
const buildMessages = args.mode === "newspaper" ? buildNewspaperMessages : args.mode === "ppt" ? buildPptMessages : buildMagazineMessages;
|
|
10274
10944
|
const messages = buildMessages({
|
|
10275
10945
|
chair: args.chair,
|
|
10276
10946
|
room: args.room,
|
|
@@ -10538,7 +11208,7 @@ function buildMethodologyFooter(args) {
|
|
|
10538
11208
|
// src/routes/briefs.ts
|
|
10539
11209
|
init_paths();
|
|
10540
11210
|
function briefHasBody(b) {
|
|
10541
|
-
if (b.mode === "
|
|
11211
|
+
if (b.mode === "magazine" || b.mode === "newspaper" || b.mode === "ppt") {
|
|
10542
11212
|
const j = b.bodyJson;
|
|
10543
11213
|
return !!(j && typeof j === "object" && typeof j.title === "string" && j.title.length > 0);
|
|
10544
11214
|
}
|
|
@@ -11552,13 +12222,14 @@ function renderLongTermMemoryBlock(agentId, userName) {
|
|
|
11552
12222
|
const memories = memoriesForContext(agentId);
|
|
11553
12223
|
if (memories.length === 0) return "";
|
|
11554
12224
|
const lines = memories.map((m) => {
|
|
11555
|
-
const
|
|
11556
|
-
return ` \xB7 [${m.kind}
|
|
12225
|
+
const tag = m.pinned ? "pinned" : m.tier === "long" ? "stable" : "recent";
|
|
12226
|
+
return ` \xB7 [${tag}] [${m.kind}] ${m.content}`;
|
|
11557
12227
|
});
|
|
12228
|
+
bumpUsage(memories.map((m) => m.id));
|
|
11558
12229
|
return [
|
|
11559
12230
|
"",
|
|
11560
12231
|
`\u2500\u2500\u2500 WHAT YOU REMEMBER ABOUT ${userName} (cross-room, your own observations) \u2500\u2500\u2500`,
|
|
11561
|
-
`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.`,
|
|
11562
12233
|
...lines,
|
|
11563
12234
|
""
|
|
11564
12235
|
].join("\n");
|
|
@@ -13691,6 +14362,15 @@ async function extractMemoriesAfterAdjourn(roomId) {
|
|
|
13691
14362
|
}
|
|
13692
14363
|
})
|
|
13693
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
|
+
}
|
|
13694
14374
|
}
|
|
13695
14375
|
async function runExtractionForAgent(agent, transcript, userName) {
|
|
13696
14376
|
const system = [
|
|
@@ -15767,7 +16447,7 @@ function roomsRouter() {
|
|
|
15767
16447
|
const explicit = typeof b.style === "string" && b.style ? b.style : null;
|
|
15768
16448
|
const fromRoom = room.briefStyle && room.briefStyle !== "auto" ? room.briefStyle : null;
|
|
15769
16449
|
const style = explicit || fromRoom || "mckinsey";
|
|
15770
|
-
const mode = b.mode === "
|
|
16450
|
+
const mode = b.mode === "magazine" || b.mode === "newspaper" || b.mode === "ppt" ? b.mode : "research-note";
|
|
15771
16451
|
abortRoom(id);
|
|
15772
16452
|
const adjournedAt = Date.now();
|
|
15773
16453
|
setRoomStatus(id, "adjourned", { adjournedAt });
|
|
@@ -15848,7 +16528,7 @@ function roomsRouter() {
|
|
|
15848
16528
|
const explicit = typeof b.style === "string" && b.style ? b.style : null;
|
|
15849
16529
|
const fromRoom = room.briefStyle && room.briefStyle !== "auto" ? room.briefStyle : null;
|
|
15850
16530
|
const style = explicit || fromRoom || "mckinsey";
|
|
15851
|
-
const mode = b.mode === "
|
|
16531
|
+
const mode = b.mode === "magazine" || b.mode === "newspaper" || b.mode === "ppt" ? b.mode : "research-note";
|
|
15852
16532
|
try {
|
|
15853
16533
|
const result = await generateBrief({
|
|
15854
16534
|
roomId: id,
|
|
@@ -16015,7 +16695,7 @@ function usageRouter() {
|
|
|
16015
16695
|
init_paths();
|
|
16016
16696
|
|
|
16017
16697
|
// src/version.ts
|
|
16018
|
-
var VERSION = "0.1.
|
|
16698
|
+
var VERSION = "0.1.10";
|
|
16019
16699
|
|
|
16020
16700
|
// src/server.ts
|
|
16021
16701
|
function createApp() {
|
|
@@ -16147,6 +16827,25 @@ async function main() {
|
|
|
16147
16827
|
process.stderr.write(`[boot] clarify recovery failed: ${e instanceof Error ? e.message : String(e)}
|
|
16148
16828
|
`);
|
|
16149
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
|
+
})();
|
|
16150
16849
|
const portArg = opts.port ? Number.parseInt(opts.port, 10) : void 0;
|
|
16151
16850
|
if (portArg !== void 0 && (Number.isNaN(portArg) || portArg < 1 || portArg > 65535)) {
|
|
16152
16851
|
console.error(`Invalid --port: ${opts.port}`);
|