openmausbot 0.1.89 → 0.1.91
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/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +14810 -0
- package/dist-server/container-mcp.js +136 -36
- package/dist-server/drivers/agents-proxy.js +31 -10
- package/dist-server/index.js +48251 -44124
- package/dist-server/openmausbot.js +628 -292
- package/dist-server/pair-cli.js +628 -292
- package/dist-server/permission-proxy.js +1 -1
- package/dist-server/server/admission.js +46 -0
- package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
- package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
- package/dist-server/server/{box.js → boat.js} +381 -334
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/bot-profile.js +7 -1
- package/dist-server/server/channel-queue.js +58 -18
- package/dist-server/server/checkpoints.js +98 -52
- package/dist-server/server/claim-idle.js +33 -0
- package/dist-server/server/cloud-backend.js +5 -5
- package/dist-server/server/cloud-home-start.js +121 -0
- package/dist-server/server/cloud-home.js +241 -0
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-overflow.js +167 -0
- package/dist-server/server/composio.js +5 -0
- package/dist-server/server/computer-observation.js +2 -2
- package/dist-server/server/config.js +156 -9
- package/dist-server/server/container-computer.js +13 -0
- package/dist-server/server/control-client.js +2 -2
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/delegations.js +11 -3
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/agents-call.js +5 -1
- package/dist-server/server/drivers/agents-catalog.js +25 -3
- package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
- package/dist-server/server/drivers/builtIn.js +2 -2
- package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
- package/dist-server/server/drivers/claude.js +178 -13
- package/dist-server/server/drivers/native.js +1 -1
- package/dist-server/server/drivers/openai-chat.js +15 -4
- package/dist-server/server/drivers/pi.js +3 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/hosted-models.js +6 -1
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1462 -576
- package/dist-server/server/local-vm-inventory.js +11 -1
- package/dist-server/server/local-vm-seat-pool.js +141 -0
- package/dist-server/server/managed-policy.js +4 -1
- package/dist-server/server/memory-capture.js +194 -0
- package/dist-server/server/memory-entries.js +76 -0
- package/dist-server/server/memory-organize.js +102 -0
- package/dist-server/server/memory-tidy.js +144 -0
- package/dist-server/server/memory-topics.js +169 -0
- package/dist-server/server/memory-upkeep.js +452 -0
- package/dist-server/server/message-db.js +49 -5
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/permission-proxy.js +3 -3
- package/dist-server/server/profile-learned.js +98 -0
- package/dist-server/server/profile-versions.js +45 -11
- package/dist-server/server/recall.js +172 -0
- package/dist-server/server/redact.js +1 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/room-cwd.js +1 -1
- package/dist-server/server/room-handoffs.js +61 -6
- package/dist-server/server/routes/bot-memory.js +215 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routine-requests.js +27 -16
- package/dist-server/server/routines.js +10 -6
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/steer-queue.js +41 -10
- package/dist-server/server/store.js +42 -3
- package/dist-server/server/surface.js +14 -1
- package/dist-server/server/system-prompt.js +16 -5
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +2 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tightening-requests.js +34 -4
- package/dist-server/server/tool-summary.js +2 -2
- package/dist-server/server/tts/elevenlabs.js +12 -7
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/turn-resources.js +87 -11
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/server/workspace.js +57 -22
- package/dist-server/shared/ask-question.js +7 -5
- package/dist-server/shared/bot-avatar.js +19 -0
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/credential-request.js +3 -3
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/tool-surface.js +1 -1
- package/dist-server/src/lib/composer-attachments.js +9 -0
- package/dist-server/vps-container-mcp.js +136 -36
- package/dist-server/workspace-backup.worker.js +165 -35
- package/package.json +1 -1
- package/dist/assets/channel-DqY_gMLg.js +0 -1
- package/dist/assets/index-BIMdIbaw.css +0 -1
- package/dist/assets/index-BVPdbq2r.js +0 -583
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// Memory upkeep: the background half of a bot's memory. On for every bot
|
|
2
|
+
// unless switched off (BotRecord.memoryUpkeep === false). The main agent
|
|
3
|
+
// keeps reading and writing its notes as before; this adds what happens
|
|
4
|
+
// without it choosing:
|
|
5
|
+
// - capture: finished 1:1 turns are read after the chat goes quiet; core
|
|
6
|
+
// facts go to MEMORY.md, detail to a topic file per subject that upkeep
|
|
7
|
+
// creates and names, with other words for it (server/memory-capture.ts);
|
|
8
|
+
// - About me: a durable fact about the person, from their own words, is
|
|
9
|
+
// added to the shared About me and listed in Settings with Remove
|
|
10
|
+
// (server/profile-learned.ts);
|
|
11
|
+
// - tidy-up: nightly, and on demand, expired entries are archived, exact
|
|
12
|
+
// duplicates merged (MEMORY.md and topic files) and contradictions in
|
|
13
|
+
// MEMORY.md struck (server/memory-tidy.ts).
|
|
14
|
+
// Every memory write is a journal row with actor "upkeep", so the Memory
|
|
15
|
+
// panel shows it and Undo works. The model steps need a one-shot text call
|
|
16
|
+
// (`generateText`: Claude and the chat-completion engines); on any other
|
|
17
|
+
// engine they are skipped and only the deterministic tidy steps run.
|
|
18
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
21
|
+
import { DATA_DIR } from "./config.js";
|
|
22
|
+
import { CaptureBuffer, capturePrompt, newCandidates, parseCandidates } from "./memory-capture.js";
|
|
23
|
+
import { factIdentity, lineFactIdentity, notebookIdentities } from "./memory-entries.js";
|
|
24
|
+
import { mergeTopicText } from "./memory-topics.js";
|
|
25
|
+
import { applyMoves, MAX_MOVES, organizeCandidates, organizePrompt, parseMoves } from "./memory-organize.js";
|
|
26
|
+
import { recordMemoryChange } from "./memory-journal.js";
|
|
27
|
+
import { applyTidy, contradictionCandidates, contradictionPrompt, parseContradictions, planChanges, planTidy } from "./memory-tidy.js";
|
|
28
|
+
import { ensureWorkspace, listMemoryTopics, memoryDate, memoryEntry, memoryTopicIndex, updateMemory, workspaceDir, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
|
|
29
|
+
export const CAPTURE_MAX_TURNS = 6;
|
|
30
|
+
export const MODEL_TIMEOUT_MS = 60_000;
|
|
31
|
+
export const TIDY_CHECK_MS = 10 * 60_000;
|
|
32
|
+
/** Below this many live entries there is nothing a contradiction pass may change. */
|
|
33
|
+
export const MIN_ENTRIES_FOR_CONTRADICTIONS = 5;
|
|
34
|
+
export const ARCHIVE_TOPIC = "archive.md";
|
|
35
|
+
/** A topic file past this size gains nothing more from capture. */
|
|
36
|
+
export const TOPIC_MAX_BYTES = 64 * 1024;
|
|
37
|
+
/** On unless switched off: every bot keeps its memory in shape by default. */
|
|
38
|
+
export function upkeepEnabled(bot) {
|
|
39
|
+
return Boolean(bot) && bot.memoryUpkeep !== false;
|
|
40
|
+
}
|
|
41
|
+
/** How many "this entry is core" judgements are remembered per bot. */
|
|
42
|
+
const MAX_CORE = 500;
|
|
43
|
+
function statePath() {
|
|
44
|
+
return join(DATA_DIR, "memory-upkeep.json");
|
|
45
|
+
}
|
|
46
|
+
function loadState() {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(readFileSync(statePath(), "utf8"));
|
|
49
|
+
return parsed && typeof parsed.bots === "object" && parsed.bots ? parsed : { bots: {} };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { bots: {} };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function saveState(state) {
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
|
|
58
|
+
writeFileAtomic(statePath(), `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// status is a convenience; losing it only means the tidy may run again
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function readRaw(botId, relative) {
|
|
65
|
+
try {
|
|
66
|
+
return readFileSync(join(workspaceDir(botId), relative), "utf8");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function askModel(engine, prompt) {
|
|
73
|
+
if (!engine.generateText)
|
|
74
|
+
return null;
|
|
75
|
+
const expiry = AbortSignal.timeout(MODEL_TIMEOUT_MS);
|
|
76
|
+
try {
|
|
77
|
+
const answer = await Promise.race([
|
|
78
|
+
engine.generateText(prompt, { signal: expiry }),
|
|
79
|
+
new Promise((_, reject) => expiry.addEventListener("abort", () => reject(new Error("timed out")), { once: true })),
|
|
80
|
+
]);
|
|
81
|
+
return typeof answer === "string" ? answer : null;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export const NO_TEXT_ENGINE = "This bot's engine cannot make the quick background model call upkeep uses, so only expired notes and exact duplicates are tidied.";
|
|
88
|
+
export function createMemoryUpkeep(deps) {
|
|
89
|
+
const now = () => deps.now?.() ?? new Date();
|
|
90
|
+
const log = (line) => deps.log?.(line);
|
|
91
|
+
const inflight = new Set();
|
|
92
|
+
const tidying = new Map();
|
|
93
|
+
let timer = null;
|
|
94
|
+
let paused = false;
|
|
95
|
+
const deferred = [];
|
|
96
|
+
const record = (botId, patch) => {
|
|
97
|
+
const state = loadState();
|
|
98
|
+
state.bots[botId] = { ...state.bots[botId], ...patch };
|
|
99
|
+
saveState(state);
|
|
100
|
+
};
|
|
101
|
+
const track = (promise) => {
|
|
102
|
+
inflight.add(promise);
|
|
103
|
+
void promise.finally(() => inflight.delete(promise)).catch(() => undefined);
|
|
104
|
+
return promise;
|
|
105
|
+
};
|
|
106
|
+
async function capture(batch) {
|
|
107
|
+
const bot = deps.bot(batch.botId);
|
|
108
|
+
const at = now().getTime();
|
|
109
|
+
const off = { at, added: 0, topics: 0, aboutMe: 0, note: "upkeep is off" };
|
|
110
|
+
if (!upkeepEnabled(bot))
|
|
111
|
+
return off;
|
|
112
|
+
const engine = deps.engine(bot.id);
|
|
113
|
+
if (!engine?.generateText) {
|
|
114
|
+
const report = { at, added: 0, topics: 0, aboutMe: 0, note: NO_TEXT_ENGINE };
|
|
115
|
+
record(bot.id, { lastCapture: report });
|
|
116
|
+
return report;
|
|
117
|
+
}
|
|
118
|
+
ensureWorkspace(bot.id);
|
|
119
|
+
const today = memoryDate(now());
|
|
120
|
+
const answer = await askModel(engine, capturePrompt({
|
|
121
|
+
botName: bot.name,
|
|
122
|
+
turns: batch.turns,
|
|
123
|
+
notebook: readRaw(bot.id, "MEMORY.md") ?? "",
|
|
124
|
+
today,
|
|
125
|
+
topics: memoryTopicIndex(bot.id),
|
|
126
|
+
}));
|
|
127
|
+
if (answer === null) {
|
|
128
|
+
const report = { at, added: 0, topics: 0, aboutMe: 0, note: "The capture call did not answer in time." };
|
|
129
|
+
record(bot.id, { lastCapture: report });
|
|
130
|
+
return report;
|
|
131
|
+
}
|
|
132
|
+
if (paused) {
|
|
133
|
+
// a backup began while the model answered: run it again after
|
|
134
|
+
deferred.push(batch);
|
|
135
|
+
return { at, added: 0, topics: 0, aboutMe: 0, note: "deferred until the backup finishes" };
|
|
136
|
+
}
|
|
137
|
+
// Re-read after the await: the bot or the person may have written since.
|
|
138
|
+
// From here to the journal rows there is no await, so no write interleaves.
|
|
139
|
+
if (!upkeepEnabled(deps.bot(bot.id)))
|
|
140
|
+
return off;
|
|
141
|
+
const before = readRaw(bot.id, "MEMORY.md");
|
|
142
|
+
const parsed = parseCandidates(answer, today);
|
|
143
|
+
const fresh = newCandidates(parsed, before ?? "");
|
|
144
|
+
const source = `${deps.sourceLabel(bot.id, batch.threadId)} (noticed)`;
|
|
145
|
+
let added = 0;
|
|
146
|
+
let full = false;
|
|
147
|
+
const byTopic = new Map();
|
|
148
|
+
for (const candidate of fresh) {
|
|
149
|
+
if (candidate.topic) {
|
|
150
|
+
byTopic.set(candidate.topic, [...(byTopic.get(candidate.topic) ?? []), candidate]);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const result = updateMemory(bot.id, { action: "append", text: candidate.text, ...(candidate.until ? { until: candidate.until } : {}) }, { source, now: now() });
|
|
154
|
+
if (result.ok)
|
|
155
|
+
added += 1;
|
|
156
|
+
else if (result.code === "over-budget")
|
|
157
|
+
full = true;
|
|
158
|
+
}
|
|
159
|
+
if (added)
|
|
160
|
+
recordMemoryChange(bot.id, { path: "MEMORY.md", actor: "upkeep", via: "capture", threadId: batch.threadId, before, after: readRaw(bot.id, "MEMORY.md") });
|
|
161
|
+
// Detail goes to a topic file the bot keeps for that subject, created
|
|
162
|
+
// with a header (title and other words for it) the first time.
|
|
163
|
+
const existing = new Map(listMemoryTopics(bot.id).map((topic) => [topic.name.toLowerCase(), topic.name]));
|
|
164
|
+
let topics = 0;
|
|
165
|
+
for (const [wanted, candidates] of byTopic) {
|
|
166
|
+
const name = existing.get(wanted) ?? wanted;
|
|
167
|
+
const path = `memory/${name}`;
|
|
168
|
+
const current = readRaw(bot.id, path);
|
|
169
|
+
if (current !== null && Buffer.byteLength(current, "utf8") > TOPIC_MAX_BYTES)
|
|
170
|
+
continue;
|
|
171
|
+
const seen = notebookIdentities(current ?? "");
|
|
172
|
+
const lines = candidates
|
|
173
|
+
.filter((candidate) => !seen.has(factIdentity(candidate.text)))
|
|
174
|
+
.map((candidate) => memoryEntry(candidate.text, { source, now: now(), ...(candidate.until ? { until: candidate.until } : {}) }));
|
|
175
|
+
if (!lines.length)
|
|
176
|
+
continue;
|
|
177
|
+
const title = name.replace(/\.md$/, "").replace(/-/g, " ");
|
|
178
|
+
const aliases = [...new Set(candidates.flatMap((candidate) => candidate.topicAliases ?? []))];
|
|
179
|
+
try {
|
|
180
|
+
writeMemoryTopic(bot.id, name, mergeTopicText(current, { title, aliases, lines }));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
recordMemoryChange(bot.id, { path, actor: "upkeep", via: "capture", threadId: batch.threadId, before: current, after: readRaw(bot.id, path) });
|
|
186
|
+
existing.set(name.toLowerCase(), name);
|
|
187
|
+
topics += lines.length;
|
|
188
|
+
}
|
|
189
|
+
// Only the owner's own words reach About me: on a shared workspace
|
|
190
|
+
// another person's facts are not the owner's profile. Every parsed fact
|
|
191
|
+
// counts, not only new ones — the bot may already have noted it in its
|
|
192
|
+
// own memory, and About me is where every other bot learns it.
|
|
193
|
+
const owner = batch.turns.every((turn) => turn.owner !== false);
|
|
194
|
+
const aboutMe = owner ? deps.addToAboutMe({ botId: bot.id, botName: bot.name }, parsed.filter((c) => c.aboutUser).map((c) => c.text)) : 0;
|
|
195
|
+
// whoever wrote MEMORY.md — the bot, the person or this capture — detail
|
|
196
|
+
// that is not core moves to its topic now, not only at night
|
|
197
|
+
const organized = await organize(bot.id);
|
|
198
|
+
const report = { at, added, topics, aboutMe, organized, ...(full ? { note: "MEMORY.md is full; the tidy-up or a person needs to make room." } : {}) };
|
|
199
|
+
record(bot.id, { lastCapture: report });
|
|
200
|
+
if (added || topics || aboutMe)
|
|
201
|
+
log(`memory upkeep: noticed ${added} fact(s) for MEMORY.md, ${topics} for topic files and ${aboutMe} for About me — ${bot.name} (${bot.id}) from ${batch.threadId}`);
|
|
202
|
+
return report;
|
|
203
|
+
}
|
|
204
|
+
const buffer = new CaptureBuffer({
|
|
205
|
+
quietMs: deps.quietMs,
|
|
206
|
+
maxTurns: CAPTURE_MAX_TURNS,
|
|
207
|
+
onFlush: (batch) => {
|
|
208
|
+
if (paused) {
|
|
209
|
+
deferred.push(batch);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
void track(capture(batch)).catch((error) => log(`memory upkeep: capture failed for ${batch.botId}: ${error.message}`));
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
/** Move MEMORY.md entries that are not core into their topic files. One
|
|
216
|
+
* model call over entries not judged before; lines move unchanged, topics
|
|
217
|
+
* first so undoing the newest row (MEMORY.md) never loses one. */
|
|
218
|
+
async function organize(botId) {
|
|
219
|
+
const engine = deps.engine(botId);
|
|
220
|
+
if (!engine?.generateText)
|
|
221
|
+
return 0;
|
|
222
|
+
const today = memoryDate(now());
|
|
223
|
+
const state = loadState();
|
|
224
|
+
const core = new Set(state.bots[botId]?.core ?? []);
|
|
225
|
+
const first = readRaw(botId, "MEMORY.md") ?? "";
|
|
226
|
+
const candidates = organizeCandidates(first, today, core);
|
|
227
|
+
if (!candidates.length)
|
|
228
|
+
return 0;
|
|
229
|
+
const answer = await askModel(engine, organizePrompt(candidates, memoryTopicIndex(botId)));
|
|
230
|
+
if (answer === null || paused || !upkeepEnabled(deps.bot(botId)))
|
|
231
|
+
return 0;
|
|
232
|
+
// the line numbers are the file's as read; a write since means ask again later
|
|
233
|
+
const before = readRaw(botId, "MEMORY.md") ?? "";
|
|
234
|
+
if (before !== first)
|
|
235
|
+
return 0;
|
|
236
|
+
const proposed = parseMoves(answer, candidates);
|
|
237
|
+
if (proposed === null)
|
|
238
|
+
return 0;
|
|
239
|
+
// Deferred moves are not core: let the next pass reconsider them.
|
|
240
|
+
const moved = new Set(proposed.map((move) => move.entry.line));
|
|
241
|
+
const moves = proposed.slice(0, MAX_MOVES);
|
|
242
|
+
const judgedCore = candidates.filter((entry) => !moved.has(entry.line)).map((entry) => factIdentity(entry.body));
|
|
243
|
+
const saved = loadState();
|
|
244
|
+
saved.bots[botId] = { ...saved.bots[botId], core: [...new Set([...(saved.bots[botId]?.core ?? []), ...judgedCore])].slice(-MAX_CORE) };
|
|
245
|
+
saveState(saved);
|
|
246
|
+
if (!moves.length)
|
|
247
|
+
return 0;
|
|
248
|
+
const { byTopic } = applyMoves(before, moves);
|
|
249
|
+
const written = new Set();
|
|
250
|
+
const existing = new Map(listMemoryTopics(botId).map((topic) => [topic.name.toLowerCase(), topic.name]));
|
|
251
|
+
let count = 0;
|
|
252
|
+
for (const [wanted, group] of byTopic) {
|
|
253
|
+
const name = existing.get(wanted) ?? wanted;
|
|
254
|
+
const path = `memory/${name}`;
|
|
255
|
+
const current = readRaw(botId, path);
|
|
256
|
+
const seen = notebookIdentities(current ?? "");
|
|
257
|
+
const lines = group.lines.filter((line) => !seen.has(lineFactIdentity(line)));
|
|
258
|
+
if (lines.length) {
|
|
259
|
+
try {
|
|
260
|
+
writeMemoryTopic(botId, name, mergeTopicText(current, { title: name.replace(/\.md$/, "").replace(/-/g, " "), aliases: group.aliases, lines }));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
log(`memory upkeep: could not write ${path}; kept its entries in MEMORY.md: ${error.message}`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
recordMemoryChange(botId, { path, actor: "upkeep", via: "organize", before: current, after: readRaw(botId, path) });
|
|
267
|
+
}
|
|
268
|
+
existing.set(name.toLowerCase(), name);
|
|
269
|
+
written.add(wanted);
|
|
270
|
+
count += group.lines.length;
|
|
271
|
+
}
|
|
272
|
+
if (!written.size)
|
|
273
|
+
return 0;
|
|
274
|
+
const { text } = applyMoves(before, moves.filter((move) => written.has(move.topic)));
|
|
275
|
+
writeMemoryFile(botId, text);
|
|
276
|
+
recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "organize", before, after: readRaw(botId, "MEMORY.md") });
|
|
277
|
+
log(`memory upkeep: moved ${count} entr${count === 1 ? "y" : "ies"} from MEMORY.md into topic files for ${deps.bot(botId)?.name ?? botId}`);
|
|
278
|
+
return count;
|
|
279
|
+
}
|
|
280
|
+
function appendArchive(botId, archived) {
|
|
281
|
+
const archivePath = `memory/${ARCHIVE_TOPIC}`;
|
|
282
|
+
const archiveBefore = readRaw(botId, archivePath);
|
|
283
|
+
const head = archiveBefore ?? "---\ntitle: Archive\ndescription: expired notes moved out by the tidy-up\n---\n";
|
|
284
|
+
writeMemoryTopic(botId, ARCHIVE_TOPIC, `${head}${head.endsWith("\n") ? "" : "\n"}${archived.join("\n")}\n`);
|
|
285
|
+
recordMemoryChange(botId, { path: archivePath, actor: "upkeep", via: "tidy", before: archiveBefore, after: readRaw(botId, archivePath) });
|
|
286
|
+
}
|
|
287
|
+
async function runTidy(botId) {
|
|
288
|
+
const bot = deps.bot(botId);
|
|
289
|
+
const at = now().getTime();
|
|
290
|
+
const today = memoryDate(now());
|
|
291
|
+
const stopped = { at, expired: 0, duplicates: 0, superseded: 0, deferred: 0, contradictionsChecked: false, note: "Memory upkeep is off or paused; no further changes were made." };
|
|
292
|
+
if (paused || !upkeepEnabled(bot))
|
|
293
|
+
return stopped;
|
|
294
|
+
ensureWorkspace(botId);
|
|
295
|
+
const organized = await organize(botId);
|
|
296
|
+
if (paused || !upkeepEnabled(deps.bot(botId)))
|
|
297
|
+
return stopped;
|
|
298
|
+
const first = readRaw(botId, "MEMORY.md") ?? "";
|
|
299
|
+
const engine = deps.engine(botId);
|
|
300
|
+
const candidates = contradictionCandidates(first, today);
|
|
301
|
+
let contradictions = [];
|
|
302
|
+
let contradictionsChecked = false;
|
|
303
|
+
let note;
|
|
304
|
+
if (!engine?.generateText)
|
|
305
|
+
note = NO_TEXT_ENGINE;
|
|
306
|
+
else if (candidates.length < MIN_ENTRIES_FOR_CONTRADICTIONS)
|
|
307
|
+
note = `Contradictions are checked from ${MIN_ENTRIES_FOR_CONTRADICTIONS} notes on.`;
|
|
308
|
+
else {
|
|
309
|
+
const answer = await askModel(engine, contradictionPrompt(candidates));
|
|
310
|
+
if (answer === null)
|
|
311
|
+
note = "The contradiction check did not answer in time.";
|
|
312
|
+
else {
|
|
313
|
+
contradictions = parseContradictions(answer, candidates.length);
|
|
314
|
+
contradictionsChecked = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (paused || !upkeepEnabled(deps.bot(botId)))
|
|
318
|
+
return stopped;
|
|
319
|
+
// Re-read after the await. If the notes changed meanwhile, the model's
|
|
320
|
+
// line numbers no longer point at the same facts: keep only the
|
|
321
|
+
// deterministic steps this time.
|
|
322
|
+
const before = readRaw(botId, "MEMORY.md") ?? "";
|
|
323
|
+
if (before !== first && contradictions.length) {
|
|
324
|
+
contradictions = [];
|
|
325
|
+
note = "The notes changed during the check; contradictions wait for the next tidy-up.";
|
|
326
|
+
}
|
|
327
|
+
const plan = planTidy(before, today, contradictions);
|
|
328
|
+
if (planChanges(plan)) {
|
|
329
|
+
const { text, archived } = applyTidy(before, plan, today);
|
|
330
|
+
// The archive is written first: a line is never out of MEMORY.md
|
|
331
|
+
// before it is in the archive, and the newest journal row — the one a
|
|
332
|
+
// person undoes — is MEMORY.md, whose undo brings the line back.
|
|
333
|
+
if (archived.length)
|
|
334
|
+
appendArchive(botId, archived);
|
|
335
|
+
writeMemoryFile(botId, text);
|
|
336
|
+
recordMemoryChange(botId, { path: "MEMORY.md", actor: "upkeep", via: "tidy", before, after: readRaw(botId, "MEMORY.md") });
|
|
337
|
+
}
|
|
338
|
+
// Topic files get the two steps that need no model: expired lines to the
|
|
339
|
+
// archive (written first, as above) and exact duplicates merged.
|
|
340
|
+
let topicExpired = 0;
|
|
341
|
+
let topicDuplicates = 0;
|
|
342
|
+
for (const topic of listMemoryTopics(botId)) {
|
|
343
|
+
if (topic.name === ARCHIVE_TOPIC)
|
|
344
|
+
continue;
|
|
345
|
+
const path = `memory/${topic.name}`;
|
|
346
|
+
const topicBefore = readRaw(botId, path);
|
|
347
|
+
if (topicBefore === null)
|
|
348
|
+
continue;
|
|
349
|
+
const topicPlan = planTidy(topicBefore, today);
|
|
350
|
+
if (!planChanges(topicPlan))
|
|
351
|
+
continue;
|
|
352
|
+
const tidied = applyTidy(topicBefore, topicPlan, today);
|
|
353
|
+
if (tidied.archived.length)
|
|
354
|
+
appendArchive(botId, tidied.archived);
|
|
355
|
+
writeMemoryTopic(botId, topic.name, tidied.text);
|
|
356
|
+
recordMemoryChange(botId, { path, actor: "upkeep", via: "tidy", before: topicBefore, after: readRaw(botId, path) });
|
|
357
|
+
topicExpired += topicPlan.expired.length;
|
|
358
|
+
topicDuplicates += topicPlan.duplicates.length;
|
|
359
|
+
}
|
|
360
|
+
const report = {
|
|
361
|
+
at,
|
|
362
|
+
expired: plan.expired.length + topicExpired,
|
|
363
|
+
duplicates: plan.duplicates.length + topicDuplicates,
|
|
364
|
+
superseded: plan.superseded.length,
|
|
365
|
+
deferred: plan.deferred,
|
|
366
|
+
...(organized ? { organized } : {}),
|
|
367
|
+
contradictionsChecked,
|
|
368
|
+
...(note ? { note } : {}),
|
|
369
|
+
};
|
|
370
|
+
record(botId, { lastTidy: report });
|
|
371
|
+
if (report.expired || report.duplicates || report.superseded || organized)
|
|
372
|
+
log(`memory upkeep: tidied ${bot?.name ?? botId} — ${report.expired} expired, ${report.duplicates} duplicate(s), ${report.superseded} contradicted`);
|
|
373
|
+
return report;
|
|
374
|
+
}
|
|
375
|
+
function tidy(botId) {
|
|
376
|
+
const running = tidying.get(botId);
|
|
377
|
+
if (running)
|
|
378
|
+
return running;
|
|
379
|
+
const promise = track(runTidy(botId)).finally(() => tidying.delete(botId));
|
|
380
|
+
tidying.set(botId, promise);
|
|
381
|
+
return promise;
|
|
382
|
+
}
|
|
383
|
+
function due(botId) {
|
|
384
|
+
const current = now();
|
|
385
|
+
if (current.getHours() < deps.tidyHour())
|
|
386
|
+
return false;
|
|
387
|
+
const last = loadState().bots[botId]?.lastTidy?.at;
|
|
388
|
+
return !last || memoryDate(new Date(last)) < memoryDate(current);
|
|
389
|
+
}
|
|
390
|
+
async function tick() {
|
|
391
|
+
if (paused)
|
|
392
|
+
return;
|
|
393
|
+
for (const bot of deps.bots()) {
|
|
394
|
+
if (paused)
|
|
395
|
+
return;
|
|
396
|
+
if (!upkeepEnabled(bot) || deps.busy(bot.id) || !due(bot.id))
|
|
397
|
+
continue;
|
|
398
|
+
try {
|
|
399
|
+
await tidy(bot.id);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
log(`memory upkeep: tidy failed for ${bot.id}: ${error.message}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
noteTurn(botId, threadId, turn) {
|
|
408
|
+
if (!upkeepEnabled(deps.bot(botId)))
|
|
409
|
+
return;
|
|
410
|
+
if (!turn.person.trim() && !turn.bot.trim())
|
|
411
|
+
return;
|
|
412
|
+
buffer.add(botId, threadId, turn);
|
|
413
|
+
},
|
|
414
|
+
flushThread: (threadId) => buffer.flush(threadId),
|
|
415
|
+
dropBot: (botId) => buffer.dropBot(botId),
|
|
416
|
+
capture: (batch) => track(capture(batch)),
|
|
417
|
+
tidy,
|
|
418
|
+
status(botId) {
|
|
419
|
+
// the core judgements are upkeep's own bookkeeping, not status
|
|
420
|
+
const { lastTidy, lastCapture } = loadState().bots[botId] ?? {};
|
|
421
|
+
return { ...(lastTidy ? { lastTidy } : {}), ...(lastCapture ? { lastCapture } : {}), modelSteps: Boolean(deps.engine(botId)?.generateText) };
|
|
422
|
+
},
|
|
423
|
+
tick,
|
|
424
|
+
start() {
|
|
425
|
+
if (timer)
|
|
426
|
+
return;
|
|
427
|
+
timer = setInterval(() => void tick(), TIDY_CHECK_MS);
|
|
428
|
+
timer.unref?.();
|
|
429
|
+
// a first pass shortly after start catches a night the computer slept through
|
|
430
|
+
const first = setTimeout(() => void tick(), 60_000);
|
|
431
|
+
first.unref?.();
|
|
432
|
+
},
|
|
433
|
+
stop() {
|
|
434
|
+
if (timer)
|
|
435
|
+
clearInterval(timer);
|
|
436
|
+
timer = null;
|
|
437
|
+
},
|
|
438
|
+
pause() {
|
|
439
|
+
paused = true;
|
|
440
|
+
},
|
|
441
|
+
resume() {
|
|
442
|
+
paused = false;
|
|
443
|
+
for (const batch of deferred.splice(0)) {
|
|
444
|
+
void track(capture(batch)).catch((error) => log(`memory upkeep: capture failed for ${batch.botId}: ${error.message}`));
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
async idle() {
|
|
448
|
+
while (inflight.size)
|
|
449
|
+
await Promise.allSettled(inflight);
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
@@ -475,7 +475,7 @@ const STOP_WORDS = new Set("a an and are as at be by did do for from had has hav
|
|
|
475
475
|
* whitespace-separated token becomes a quoted string, so `AND`, `NOT`,
|
|
476
476
|
* `*`, `:`, and stray quotes are searched for rather than interpreted.
|
|
477
477
|
* Tokens are ANDed — FTS5's default — so a hit contains all of them. */
|
|
478
|
-
function ftsQuery(query) {
|
|
478
|
+
function ftsQuery(query, mode = "all") {
|
|
479
479
|
const tokens = query
|
|
480
480
|
.split(/\s+/)
|
|
481
481
|
.map((token) => token.replace(/"/g, "").trim())
|
|
@@ -483,8 +483,49 @@ function ftsQuery(query) {
|
|
|
483
483
|
if (!tokens.length)
|
|
484
484
|
return null;
|
|
485
485
|
const content = tokens.filter((token) => !STOP_WORDS.has(token.toLowerCase()));
|
|
486
|
+
if (mode === "any") {
|
|
487
|
+
// Automatic recall searches with a whole message: any content word may
|
|
488
|
+
// match, ranked by bm25. Punctuation-only tokens are dropped, since a
|
|
489
|
+
// lone "?" or "—" would match nothing useful. No content word, no query.
|
|
490
|
+
const terms = recallTerms(query).map(recallMatchTerm).filter(Boolean);
|
|
491
|
+
return terms.length ? terms.join(" OR ") : null;
|
|
492
|
+
}
|
|
486
493
|
return (content.length ? content : tokens).map((token) => `"${token}"`).join(" ");
|
|
487
494
|
}
|
|
495
|
+
/** One recall term as FTS5 syntax. The index does no stemming, so a word
|
|
496
|
+
* of four or more letters matches as a prefix, with a plural -s taken off
|
|
497
|
+
* first: "restaurant" and "restaurants" find each other. Skip punctuation
|
|
498
|
+
* terms: unicode61 cannot distinguish "-10" from "10" or "c++" from "c",
|
|
499
|
+
* even in a quoted FTS phrase. Explicit all-term search stays unchanged. */
|
|
500
|
+
export function recallMatchTerm(term) {
|
|
501
|
+
if (!/^[\p{L}\p{N}]+$/u.test(term))
|
|
502
|
+
return null;
|
|
503
|
+
if (!/^\p{L}{4,}$/u.test(term))
|
|
504
|
+
return `"${term}"`;
|
|
505
|
+
const stem = term.length > 4 && term.endsWith("s") && !term.endsWith("ss") ? term.slice(0, -1) : term;
|
|
506
|
+
return `"${stem}"*`;
|
|
507
|
+
}
|
|
508
|
+
/** Words that go at the end of a question and say nothing about its topic. */
|
|
509
|
+
const RECALL_FILLER = new Set("about again also any anything can could does don't know me my please remember remind should tell there they us would hi hello hey thanks thank ok okay yes yeah no i'm im i've i'll i'd you're quick just".split(" "));
|
|
510
|
+
/** The content words of a message, for an any-term recall: lower-cased,
|
|
511
|
+
* de-duplicated, stop words and filler dropped. Ordinary separators between
|
|
512
|
+
* letters split words; signs, language symbols and versions (`-10`, `c++`,
|
|
513
|
+
* `v2.1`) survive as written rather than matching a different fact. */
|
|
514
|
+
export function recallTerms(query) {
|
|
515
|
+
const out = [];
|
|
516
|
+
for (const raw of query.split(/\s+|(?<=\p{L})[/,;:.—–-]+(?=\p{L})/u)) {
|
|
517
|
+
const token = raw.replace(/"/g, "").replace(/^[\s.,;:!?()[\]{}'`“”‘’]+|[\s.,;:!?()[\]{}'`“”‘’]+$/g, "").toLowerCase();
|
|
518
|
+
if (token.length < 2 && !/\d/.test(token))
|
|
519
|
+
continue;
|
|
520
|
+
if (!/[\p{L}\p{N}]/u.test(token))
|
|
521
|
+
continue;
|
|
522
|
+
if (STOP_WORDS.has(token) || RECALL_FILLER.has(token))
|
|
523
|
+
continue;
|
|
524
|
+
if (!out.includes(token))
|
|
525
|
+
out.push(token);
|
|
526
|
+
}
|
|
527
|
+
return out.slice(0, 16);
|
|
528
|
+
}
|
|
488
529
|
/** How much of a matched message rides back in a hit. Wide enough that a
|
|
489
530
|
* short report reads whole; a longer one is fetched with readMessageText. */
|
|
490
531
|
const SNIPPET_TOKENS = 48;
|
|
@@ -523,8 +564,8 @@ function rangeClause(range, column) {
|
|
|
523
564
|
}
|
|
524
565
|
return { sql: parts.map((part) => ` AND ${part}`).join(""), params };
|
|
525
566
|
}
|
|
526
|
-
export function recallMessages(query, threadIds, limit = 12, range) {
|
|
527
|
-
const match = ftsQuery(query);
|
|
567
|
+
export function recallMessages(query, threadIds, limit = 12, range, mode = "all") {
|
|
568
|
+
const match = ftsQuery(query, mode);
|
|
528
569
|
if (!match || !threadIds.length)
|
|
529
570
|
return [];
|
|
530
571
|
const placeholders = threadIds.map(() => "?").join(", ");
|
|
@@ -632,8 +673,8 @@ export function indexedMemoryFiles(botId) {
|
|
|
632
673
|
/** Relevance-ranked recall over ONE bot's memory files. Scoped by bot id
|
|
633
674
|
* in SQL, the same way recallMessages scopes by thread: another bot's
|
|
634
675
|
* memory is not a lower-ranked result, it is not a result. */
|
|
635
|
-
export function recallMemory(query, botId, limit = 12) {
|
|
636
|
-
const match = ftsQuery(query);
|
|
676
|
+
export function recallMemory(query, botId, limit = 12, mode = "all") {
|
|
677
|
+
const match = ftsQuery(query, mode);
|
|
637
678
|
if (!match)
|
|
638
679
|
return [];
|
|
639
680
|
const rows = db()
|
|
@@ -641,6 +682,9 @@ export function recallMemory(query, botId, limit = 12) {
|
|
|
641
682
|
`snippet(memory_fts, 0, '[', ']', '…', ${SNIPPET_TOKENS}) AS snippet ` +
|
|
642
683
|
"FROM memory_fts JOIN memory_files f ON f.rowid = memory_fts.rowid " +
|
|
643
684
|
"WHERE memory_fts MATCH ? AND f.bot_id = ? " +
|
|
685
|
+
// Automatic recall must filter before LIMIT; historical logs can
|
|
686
|
+
// otherwise crowd all current topic files out of the result set.
|
|
687
|
+
(mode === "any" ? "AND f.path NOT IN ('MEMORY.md', 'memory/archive.md') AND f.path NOT LIKE 'memory/log/%' " : "") +
|
|
644
688
|
"ORDER BY bm25(memory_fts), f.mtime_ms DESC LIMIT ?")
|
|
645
689
|
// SAFETY: the SELECT names exactly these three columns; snippet() is never null
|
|
646
690
|
.all(match, botId, limit);
|
|
@@ -6,10 +6,10 @@ export function modelContextWindow(model) {
|
|
|
6
6
|
if (!model)
|
|
7
7
|
return undefined;
|
|
8
8
|
const id = model.toLowerCase();
|
|
9
|
-
// Opus 5.5
|
|
10
|
-
// slug after a slash or a provider dot. A longer suffix such
|
|
11
|
-
// `claude-
|
|
12
|
-
if (/(?:^|[/.])claude-opus-5(?:-5|\.5)$/.test(id))
|
|
9
|
+
// Opus 5.5 and Sonnet 5.5 are 1M without a `[1m]` suffix. Only the exact
|
|
10
|
+
// slug, or that slug after a slash or a provider dot. A longer suffix such
|
|
11
|
+
// as `claude-sonnet-5-5-local` stays on the generic Claude rule, as does `host::`.
|
|
12
|
+
if (/(?:^|[/.])claude-(?:opus|sonnet)-5(?:-5|\.5)$/.test(id))
|
|
13
13
|
return 1_000_000;
|
|
14
14
|
if (/claude/.test(id) || /^(opus|sonnet|haiku|fable)\b/.test(id))
|
|
15
15
|
return /\[1m\]|-1m\b|1m-context/.test(id) ? 1_000_000 : 200_000;
|
|
@@ -15,6 +15,21 @@ function portableKey(value, fallback, used) {
|
|
|
15
15
|
used.add(key);
|
|
16
16
|
return key;
|
|
17
17
|
}
|
|
18
|
+
/** A room's responder in package terms. An Auto room is written as its lead
|
|
19
|
+
* (its fallback, else its first exported member): that is how it behaves on
|
|
20
|
+
* any install without the decision model, and every older app can read it,
|
|
21
|
+
* where a new `auto` kind would make them refuse the whole package. */
|
|
22
|
+
function packageRoomResponder(responder, keys, members) {
|
|
23
|
+
if (responder.kind === "member" && keys.has(responder.botId))
|
|
24
|
+
return { kind: "agent", agent: keys.get(responder.botId) };
|
|
25
|
+
if (responder.kind === "everyone")
|
|
26
|
+
return { kind: "everyone" };
|
|
27
|
+
if (responder.kind === "auto") {
|
|
28
|
+
const lead = responder.fallbackBotId ? keys.get(responder.fallbackBotId) : undefined;
|
|
29
|
+
return { kind: "agent", agent: lead ?? members[0] };
|
|
30
|
+
}
|
|
31
|
+
return { kind: "mentions" };
|
|
32
|
+
}
|
|
18
33
|
function samePlaybook(a, b) {
|
|
19
34
|
return a.name === b.name && a.summary === b.summary && a.instructions === b.instructions &&
|
|
20
35
|
a.triggers.join("\n") === b.triggers.join("\n");
|
|
@@ -122,11 +137,7 @@ export function createBotPackageExport(input) {
|
|
|
122
137
|
const members = group.memberIds.flatMap((id) => idToKey.has(id) ? [idToKey.get(id)] : []);
|
|
123
138
|
if (!members.length)
|
|
124
139
|
continue;
|
|
125
|
-
const defaultResponder = group.defaultResponder
|
|
126
|
-
? { kind: "agent", agent: idToKey.get(group.defaultResponder.botId) }
|
|
127
|
-
: group.defaultResponder.kind === "everyone"
|
|
128
|
-
? { kind: "everyone" }
|
|
129
|
-
: { kind: "mentions" };
|
|
140
|
+
const defaultResponder = packageRoomResponder(group.defaultResponder, idToKey, members);
|
|
130
141
|
rooms.push({
|
|
131
142
|
key: portableKey(group.name, `room-${index + 1}`, roomKeys),
|
|
132
143
|
name: group.name,
|
|
@@ -587,11 +598,7 @@ export function createTeamPackageExport(input) {
|
|
|
587
598
|
const members = group.memberIds.flatMap((id) => botKeys.has(id) ? [botKeys.get(id)] : []);
|
|
588
599
|
if (!members.length)
|
|
589
600
|
continue;
|
|
590
|
-
const defaultResponder = group.defaultResponder
|
|
591
|
-
? { kind: "agent", agent: botKeys.get(group.defaultResponder.botId) }
|
|
592
|
-
: group.defaultResponder.kind === "everyone"
|
|
593
|
-
? { kind: "everyone" }
|
|
594
|
-
: { kind: "mentions" };
|
|
601
|
+
const defaultResponder = packageRoomResponder(group.defaultResponder, botKeys, members);
|
|
595
602
|
const key = roomKeys.get(group.id);
|
|
596
603
|
rooms.push({ key, name: group.name, members, ...(group.bulletin ? { bulletin: group.bulletin } : {}), defaultResponder });
|
|
597
604
|
roomMembers.set(group.id, new Set(members));
|
|
@@ -318,9 +318,12 @@ function runImport(source, options, deps) {
|
|
|
318
318
|
const ids = room.members.map((key) => botIds.get(key));
|
|
319
319
|
let created = store.createGroup(room.name, ids, false, section);
|
|
320
320
|
createdGroups.push(created);
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
const responder = room.defaultResponder;
|
|
322
|
+
const defaultResponder = responder.kind === "agent"
|
|
323
|
+
? { kind: "member", botId: botIds.get(responder.agent) }
|
|
324
|
+
: responder.kind === "auto"
|
|
325
|
+
? { kind: "auto", ...(responder.agent ? { fallbackBotId: botIds.get(responder.agent) } : {}) }
|
|
326
|
+
: { kind: responder.kind };
|
|
324
327
|
created = store.patchGroup(created.id, {
|
|
325
328
|
bulletin: room.bulletin ?? "",
|
|
326
329
|
defaultResponder,
|
|
@@ -53,7 +53,9 @@ export const roomReleaseValues = (room) => ({
|
|
|
53
53
|
members: [...room.members].sort(),
|
|
54
54
|
defaultResponder: room.defaultResponder.kind === "agent"
|
|
55
55
|
? { kind: "agent", agent: room.defaultResponder.agent }
|
|
56
|
-
:
|
|
56
|
+
: room.defaultResponder.kind === "auto" && room.defaultResponder.agent
|
|
57
|
+
? { kind: "auto", agent: room.defaultResponder.agent }
|
|
58
|
+
: { kind: room.defaultResponder.kind },
|
|
57
59
|
});
|
|
58
60
|
/** One normal form for a routine's timing on both sides, so an untouched
|
|
59
61
|
* routine compares equal: absent cap → null, absent overlap → "skip". */
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// CLI's own AskUserQuestion. It is the tool the model actually reaches for
|
|
15
15
|
// when it wants a person to choose, and acceptEdits will not run it unasked,
|
|
16
16
|
// so it lands here looking like "may I run a tool?" — which is how a question
|
|
17
|
-
// ended up on screen as an Allow/Deny
|
|
17
|
+
// ended up on screen as an Allow/Deny dialog over a JSON blob. It is intercepted
|
|
18
18
|
// below and asked as what it is: one card carrying every question it posed,
|
|
19
19
|
// answered through the tool's own `answers` field.
|
|
20
20
|
//
|
|
@@ -89,8 +89,8 @@ const asRecord = (value) => typeof value === "object" && value !== null && !Arra
|
|
|
89
89
|
*
|
|
90
90
|
* There is deliberately no "fall through to the permission path" here. That
|
|
91
91
|
* path cannot answer this tool — allowing it throws the click away, as
|
|
92
|
-
* above — so the fallback offered a person an Allow/Deny
|
|
93
|
-
* That
|
|
92
|
+
* above — so the fallback offered a person an Allow/Deny dialog over raw JSON.
|
|
93
|
+
* That dialog is the bug this file exists to remove.
|
|
94
94
|
*/
|
|
95
95
|
async function answerNativeQuestions(input) {
|
|
96
96
|
// Unanswerable entries are SKIPPED by the parser, not fatal: one bad entry
|