the-campfire 0.2.1 → 0.3.1
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/README.md +28 -4
- package/dist/assets/{MermaidDiagram-D_MwUXi3.js → MermaidDiagram-Cas1Ngze.js} +2 -2
- package/dist/assets/{_basePickBy-DsMRBmhy.js → _basePickBy-D1h3RRT9.js} +1 -1
- package/dist/assets/{_baseUniq-CZ0BR5Mx.js → _baseUniq-Z9p6uaM9.js} +1 -1
- package/dist/assets/{arc-D6Y0ooDg.js → arc-DJLNkmre.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-3T5ZoEn1.js → architectureDiagram-VXUJARFQ-CBHHYg8R.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-CvOCnB8f.js → blockDiagram-VD42YOAC-DMtvAHPO.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DPXJvInm.js → c4Diagram-YG6GDRKO-G9Zj2LWB.js} +1 -1
- package/dist/assets/channel-CcCVj0rn.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DPHGuPB4.js → chunk-4BX2VUAB-s3svQOtk.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BbI83gIK.js → chunk-55IACEB6-C6fhtS_L.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-DGvVY1Y_.js → chunk-B4BG7PRW-CpePj3eQ.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-2YN0FhPB.js → chunk-DI55MBZ5-Dq9AChBD.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Ctqs0bWj.js → chunk-FMBD7UC4-DSOMddvX.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-CfvfTLi4.js → chunk-QN33PNHL-ZrdWE4nY.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-C-dG1NiD.js → chunk-QZHKN3VN-BqLE4kwR.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-Bh07YiOi.js → chunk-TZMSLE5B-DY7GliuX.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-8I6xWkSV.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-8I6xWkSV.js +1 -0
- package/dist/assets/clone-zAvPgvOP.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CTC2n8Hd.js → cose-bilkent-S5V4N54A-DmRYvz2l.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-CAgGZKYt.js → dagre-6UL2VRFP-D4IfaYgb.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-DymwxXvC.js → diagram-PSM6KHXK-ClfgeCpw.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-CAEL5_ej.js → diagram-QEK2KX5R-BaBfarh-.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-B6onWI1I.js → diagram-S2PKOQOG-DT1eYmOM.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-DP-dbxgu.js → erDiagram-Q2GNP2WA-C5ZVoHhn.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-OMmA9Rvb.js → flowDiagram-NV44I4VS-C9MYAakn.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-B2oAHD8b.js → ganttDiagram-JELNMOA3-HHt6rJj9.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-CAZO9ISt.js → gitGraphDiagram-V2S2FVAM-CfA3qwoT.js} +1 -1
- package/dist/assets/{graph-CeeDmBlM.js → graph-BLJloKEi.js} +1 -1
- package/dist/assets/index-CvOAfVHK.css +32 -0
- package/dist/assets/{index-CAGJQXdJ.js → index-CxNtY8EJ.js} +82 -82
- package/dist/assets/{infoDiagram-HS3SLOUP-m0CP23H-.js → infoDiagram-HS3SLOUP-ASXl-hwl.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-C9WvzT-H.js → journeyDiagram-XKPGCS4Q-B-wDjjzW.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-KIEeugzr.js → kanban-definition-3W4ZIXB7-BNWLVKm2.js} +1 -1
- package/dist/assets/{layout-P-20nhx7.js → layout-cpBTqNBI.js} +1 -1
- package/dist/assets/{linear-CMM5aRIM.js → linear-DAonGknK.js} +1 -1
- package/dist/assets/{mermaid.core-Z9YQUdi5.js → mermaid.core-BdetWGOO.js} +4 -4
- package/dist/assets/{mindmap-definition-VGOIOE7T-B-NyLDuB.js → mindmap-definition-VGOIOE7T-CJOEqJKm.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-Dp8zgF7X.js → pieDiagram-ADFJNKIX-Dnc1ekZC.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-Cs07PJ50.js → quadrantDiagram-AYHSOK5B-Dr56Xspi.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-CWMm228C.js → requirementDiagram-UZGBJVZJ-z-G-ThCz.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-Cf7zGgTA.js → sankeyDiagram-TZEHDZUN-D_4p6Me1.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-TzZvjAhX.js → sequenceDiagram-WL72ISMW-DD1K4OoS.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-CH45r52Y.js → stateDiagram-FKZM4ZOC-ENIvCriI.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BgJoZY9E.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-CLaDCX_H.js → timeline-definition-IT6M3QCI-V8I1MBPc.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-BNNAKE7E.js → treemap-GDKQZRPO-1lnlvkR3.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-BG_0_QvU.js → xychartDiagram-PRI3JC2R-CrSrN3UV.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/collective-intelligence.ts +134 -43
- package/server/embedding.ts +12 -3
- package/server/memory-consolidation.ts +703 -0
- package/server/memory-migration.ts +468 -0
- package/server/routes/ci-routes.ts +68 -10
- package/server/routes/settings-routes.ts +10 -2
- package/server/semantic-memory.ts +1149 -187
- package/server/session-types.ts +15 -0
- package/server/settings-manager.ts +122 -3
- package/server/ws-bridge.ts +209 -26
- package/dist/assets/channel-D6cof2B8.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-BG_dxrC_.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-BG_dxrC_.js +0 -1
- package/dist/assets/clone-vuZ4vhTG.js +0 -1
- package/dist/assets/index-Byjg9zgI.css +0 -32
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DaPUshfK.js +0 -1
package/server/session-types.ts
CHANGED
|
@@ -223,6 +223,10 @@ export type BrowserIncomingMessageBase =
|
|
|
223
223
|
| { type: "memory_stored"; fragment: import("./semantic-memory.js").MemoryFragment }
|
|
224
224
|
| { type: "memory_query_result"; query: string; results: import("./semantic-memory.js").MemoryFragment[] }
|
|
225
225
|
| { type: "memory_consolidated"; tag: string; knowledge: import("./semantic-memory.js").ConsolidatedKnowledge }
|
|
226
|
+
// Emitted when a user_message was enriched with recalled memories, so the
|
|
227
|
+
// UI can render a collapsible "recalled context" chip instead of the
|
|
228
|
+
// injected text being invisible. `items` lists exactly what was included.
|
|
229
|
+
| { type: "memory_enriched"; user_message_id?: string; items: MemoryEnrichmentItem[]; truncated?: boolean }
|
|
226
230
|
// Layer 2: Deliberation
|
|
227
231
|
| { type: "deliberation_proposal"; proposal: import("./deliberation-engine.js").DeliberationProposal }
|
|
228
232
|
| { type: "deliberation_response"; response: import("./deliberation-engine.js").DeliberationResponse }
|
|
@@ -237,6 +241,17 @@ export type BrowserIncomingMessageBase =
|
|
|
237
241
|
| { type: "semantic_link_added"; sourceId: string; targetId: string; relation: string }
|
|
238
242
|
| { type: "consensus_update"; state: import("./shared-context.js").ConsensusState };
|
|
239
243
|
|
|
244
|
+
/** One recalled memory included in an enriched prompt (for the UI chip). */
|
|
245
|
+
export interface MemoryEnrichmentItem {
|
|
246
|
+
id: string;
|
|
247
|
+
kind: "knowledge" | "fragment";
|
|
248
|
+
namespace: string;
|
|
249
|
+
summary: string;
|
|
250
|
+
tag?: string;
|
|
251
|
+
/** Decayed weight at recall time (0..1). */
|
|
252
|
+
weight: number;
|
|
253
|
+
}
|
|
254
|
+
|
|
240
255
|
export type BrowserIncomingMessage = BrowserIncomingMessageBase & { seq?: number };
|
|
241
256
|
|
|
242
257
|
export type ReplayableBrowserIncomingMessage = Exclude<BrowserIncomingMessageBase, { type: "event_replay" }>;
|
|
@@ -11,6 +11,85 @@ export const DEFAULT_OPENROUTER_MODEL = "openrouter/free";
|
|
|
11
11
|
|
|
12
12
|
export type EmbeddingProvider = "openai" | "ollama" | "none";
|
|
13
13
|
|
|
14
|
+
// ─── Semantic memory settings (design doc §3.1/§3.2) ─────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Decay policy for one namespace class. halfLifeHours = null → never decays. */
|
|
17
|
+
export interface MemoryDecayPolicy {
|
|
18
|
+
halfLifeHours: number | null;
|
|
19
|
+
reinforceMultiplier: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MemorySettings {
|
|
23
|
+
decay: {
|
|
24
|
+
global: MemoryDecayPolicy;
|
|
25
|
+
repo: MemoryDecayPolicy;
|
|
26
|
+
session: MemoryDecayPolicy;
|
|
27
|
+
agent: MemoryDecayPolicy;
|
|
28
|
+
};
|
|
29
|
+
/** Per-namespace recall depth for retrieval (ADR-161's "depth is a tunable"). */
|
|
30
|
+
recallDepth: {
|
|
31
|
+
session: number;
|
|
32
|
+
repo: number;
|
|
33
|
+
agent: number;
|
|
34
|
+
global: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Defaults per §3.1: 90d/30d/7d/60d half-lives (in hours), ×1.5/1.5/1.2/1.2. */
|
|
39
|
+
export const DEFAULT_MEMORY_SETTINGS: MemorySettings = {
|
|
40
|
+
decay: {
|
|
41
|
+
global: { halfLifeHours: 90 * 24, reinforceMultiplier: 1.5 },
|
|
42
|
+
repo: { halfLifeHours: 30 * 24, reinforceMultiplier: 1.5 },
|
|
43
|
+
session: { halfLifeHours: 7 * 24, reinforceMultiplier: 1.2 },
|
|
44
|
+
agent: { halfLifeHours: 60 * 24, reinforceMultiplier: 1.2 },
|
|
45
|
+
},
|
|
46
|
+
recallDepth: { session: 4, repo: 6, agent: 2, global: 3 },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function normalizeDecayPolicy(raw: unknown, fallback: MemoryDecayPolicy): MemoryDecayPolicy {
|
|
50
|
+
const r = (raw ?? {}) as Partial<MemoryDecayPolicy>;
|
|
51
|
+
const halfLifeHours =
|
|
52
|
+
r.halfLifeHours === null
|
|
53
|
+
? null
|
|
54
|
+
: typeof r.halfLifeHours === "number" && Number.isFinite(r.halfLifeHours) && r.halfLifeHours > 0
|
|
55
|
+
? r.halfLifeHours
|
|
56
|
+
: fallback.halfLifeHours;
|
|
57
|
+
const reinforceMultiplier =
|
|
58
|
+
typeof r.reinforceMultiplier === "number" &&
|
|
59
|
+
Number.isFinite(r.reinforceMultiplier) &&
|
|
60
|
+
r.reinforceMultiplier >= 1
|
|
61
|
+
? r.reinforceMultiplier
|
|
62
|
+
: fallback.reinforceMultiplier;
|
|
63
|
+
return { halfLifeHours, reinforceMultiplier };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeRecallDepth(raw: unknown, key: keyof MemorySettings["recallDepth"]): number {
|
|
67
|
+
const r = (raw ?? {}) as Record<string, unknown>;
|
|
68
|
+
const v = r[key];
|
|
69
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) return Math.floor(v);
|
|
70
|
+
return DEFAULT_MEMORY_SETTINGS.recallDepth[key];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Deep-merge a (possibly partial/invalid) saved memory section over the defaults. */
|
|
74
|
+
export function normalizeMemorySettings(raw: unknown): MemorySettings {
|
|
75
|
+
const r = (raw ?? {}) as { decay?: Record<string, unknown>; recallDepth?: unknown };
|
|
76
|
+
const decayRaw = r.decay ?? {};
|
|
77
|
+
return {
|
|
78
|
+
decay: {
|
|
79
|
+
global: normalizeDecayPolicy(decayRaw.global, DEFAULT_MEMORY_SETTINGS.decay.global),
|
|
80
|
+
repo: normalizeDecayPolicy(decayRaw.repo, DEFAULT_MEMORY_SETTINGS.decay.repo),
|
|
81
|
+
session: normalizeDecayPolicy(decayRaw.session, DEFAULT_MEMORY_SETTINGS.decay.session),
|
|
82
|
+
agent: normalizeDecayPolicy(decayRaw.agent, DEFAULT_MEMORY_SETTINGS.decay.agent),
|
|
83
|
+
},
|
|
84
|
+
recallDepth: {
|
|
85
|
+
session: normalizeRecallDepth(r.recallDepth, "session"),
|
|
86
|
+
repo: normalizeRecallDepth(r.recallDepth, "repo"),
|
|
87
|
+
agent: normalizeRecallDepth(r.recallDepth, "agent"),
|
|
88
|
+
global: normalizeRecallDepth(r.recallDepth, "global"),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
14
93
|
export interface CampfireSettings {
|
|
15
94
|
openrouterApiKey: string;
|
|
16
95
|
openrouterModel: string;
|
|
@@ -28,6 +107,13 @@ export interface CampfireSettings {
|
|
|
28
107
|
embeddingApiKey: string; // OpenAI API key (if provider = "openai")
|
|
29
108
|
embeddingModel: string; // e.g. "text-embedding-3-small" or "nomic-embed-text"
|
|
30
109
|
embeddingBaseUrl: string; // Ollama base URL (if provider = "ollama"), default http://localhost:11434
|
|
110
|
+
/**
|
|
111
|
+
* Semantic memory v2: decay policies + recall depths per namespace class.
|
|
112
|
+
* Always populated by normalize() at runtime — typed optional only so
|
|
113
|
+
* pre-v2 CampfireSettings literals (test mocks) keep compiling. Use
|
|
114
|
+
* getMemorySettings() for guaranteed-present typed access.
|
|
115
|
+
*/
|
|
116
|
+
memory?: MemorySettings;
|
|
31
117
|
/** Whether the onboarding wizard has been completed or skipped */
|
|
32
118
|
onboardingCompleted: boolean;
|
|
33
119
|
updatedAt: number;
|
|
@@ -49,6 +135,7 @@ let settings: CampfireSettings = {
|
|
|
49
135
|
embeddingApiKey: "",
|
|
50
136
|
embeddingModel: "",
|
|
51
137
|
embeddingBaseUrl: "http://localhost:11434",
|
|
138
|
+
memory: normalizeMemorySettings(null),
|
|
52
139
|
onboardingCompleted: false,
|
|
53
140
|
updatedAt: 0,
|
|
54
141
|
};
|
|
@@ -71,6 +158,7 @@ function normalize(raw: Partial<CampfireSettings> | null | undefined): CampfireS
|
|
|
71
158
|
embeddingBaseUrl: typeof raw?.embeddingBaseUrl === "string" && raw.embeddingBaseUrl.trim()
|
|
72
159
|
? raw.embeddingBaseUrl
|
|
73
160
|
: "http://localhost:11434",
|
|
161
|
+
memory: normalizeMemorySettings(raw?.memory),
|
|
74
162
|
onboardingCompleted: raw?.onboardingCompleted === true,
|
|
75
163
|
updatedAt: typeof raw?.updatedAt === "number" ? raw.updatedAt : 0,
|
|
76
164
|
};
|
|
@@ -99,14 +187,45 @@ export function getSettings(): CampfireSettings {
|
|
|
99
187
|
return { ...settings };
|
|
100
188
|
}
|
|
101
189
|
|
|
190
|
+
/** Semantic memory settings with defaults guaranteed (never undefined). */
|
|
191
|
+
export function getMemorySettings(): MemorySettings {
|
|
192
|
+
ensureLoaded();
|
|
193
|
+
return settings.memory ?? normalizeMemorySettings(null);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Deep-merge a partial memory patch over the current memory settings, then normalize. */
|
|
197
|
+
function mergeMemoryPatch(current: MemorySettings, patch: unknown): MemorySettings {
|
|
198
|
+
if (!patch || typeof patch !== "object") return current;
|
|
199
|
+
const p = patch as {
|
|
200
|
+
decay?: Record<string, Record<string, unknown> | undefined>;
|
|
201
|
+
recallDepth?: Record<string, unknown>;
|
|
202
|
+
};
|
|
203
|
+
return normalizeMemorySettings({
|
|
204
|
+
decay: {
|
|
205
|
+
global: { ...current.decay.global, ...p.decay?.global },
|
|
206
|
+
repo: { ...current.decay.repo, ...p.decay?.repo },
|
|
207
|
+
session: { ...current.decay.session, ...p.decay?.session },
|
|
208
|
+
agent: { ...current.decay.agent, ...p.decay?.agent },
|
|
209
|
+
},
|
|
210
|
+
recallDepth: { ...current.recallDepth, ...p.recallDepth },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
102
214
|
export function updateSettings(
|
|
103
|
-
patch: Partial<Pick<CampfireSettings, "openrouterApiKey" | "openrouterModel" | "moltbookApiKey" | "linearApiKey" | "claudeOAuthToken" | "openaiApiKey" | "anthropicApiKey" | "embeddingProvider" | "embeddingApiKey" | "embeddingModel" | "embeddingBaseUrl" | "onboardingCompleted"
|
|
215
|
+
patch: Partial<Pick<CampfireSettings, "openrouterApiKey" | "openrouterModel" | "moltbookApiKey" | "linearApiKey" | "claudeOAuthToken" | "openaiApiKey" | "anthropicApiKey" | "embeddingProvider" | "embeddingApiKey" | "embeddingModel" | "embeddingBaseUrl" | "onboardingCompleted">> & {
|
|
216
|
+
/** Partial memory settings — deep-merged over the current values. */
|
|
217
|
+
memory?: unknown;
|
|
218
|
+
},
|
|
104
219
|
): CampfireSettings {
|
|
105
220
|
ensureLoaded();
|
|
221
|
+
const { memory: memoryPatch, ...rest } = patch;
|
|
106
222
|
settings = {
|
|
107
223
|
...settings,
|
|
108
|
-
...
|
|
109
|
-
openrouterModel:
|
|
224
|
+
...rest,
|
|
225
|
+
openrouterModel: rest.openrouterModel?.trim() || settings.openrouterModel || DEFAULT_OPENROUTER_MODEL,
|
|
226
|
+
memory: memoryPatch !== undefined
|
|
227
|
+
? mergeMemoryPatch(settings.memory ?? normalizeMemorySettings(null), memoryPatch)
|
|
228
|
+
: settings.memory,
|
|
110
229
|
updatedAt: Date.now(),
|
|
111
230
|
};
|
|
112
231
|
persist();
|
package/server/ws-bridge.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type { CodexAdapter } from "./codex-adapter.js";
|
|
|
35
35
|
import type { AgentAdapter } from "./adapter-types.js";
|
|
36
36
|
import type { RecorderManager } from "./recorder.js";
|
|
37
37
|
import type { CollectiveIntelligenceLayer } from "./collective-intelligence.js";
|
|
38
|
+
import { consolidate, noteSessionActivity, shouldConsolidateOnTurn } from "./memory-consolidation.js";
|
|
38
39
|
import { evaluateAutoInjection, scanMcpServers } from "./mcp-policy.js";
|
|
39
40
|
|
|
40
41
|
// ─── WebSocket data tags ──────────────────────────────────────────────────────
|
|
@@ -294,6 +295,8 @@ export class WsBridge {
|
|
|
294
295
|
]);
|
|
295
296
|
private static readonly VOTE_DEADLINE_MS = 30_000; // 30 seconds to vote
|
|
296
297
|
private static readonly INVITE_TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
298
|
+
/** Memory-enrichment budget (§3.6.1): past this, the original prompt is sent unenriched. */
|
|
299
|
+
private static readonly ENRICHMENT_TIMEOUT_MS = 250;
|
|
297
300
|
private readonly sessions = new Map<string, Session>();
|
|
298
301
|
private readonly inviteTokens = new Map<string, { sessionId: string; role: SessionRole; expiresAt: number }>();
|
|
299
302
|
private viewerCounter = 0;
|
|
@@ -309,6 +312,8 @@ export class WsBridge {
|
|
|
309
312
|
private readonly environmentMcpInjected = new Set<string>();
|
|
310
313
|
private readonly autoNamingAttempted = new Set<string>();
|
|
311
314
|
private userMsgCounter = 0;
|
|
315
|
+
/** Per-session promise chain serializing enriched user_message dispatch (§3.6.1). */
|
|
316
|
+
private readonly userMessageChains = new Map<string, Promise<void>>();
|
|
312
317
|
private onGitInfoReady: ((sessionId: string, cwd: string, branch: string) => void) | null = null;
|
|
313
318
|
private static readonly GIT_SESSION_KEYS: GitSessionKey[] = [
|
|
314
319
|
"git_branch",
|
|
@@ -1033,6 +1038,7 @@ export class WsBridge {
|
|
|
1033
1038
|
this.persistSession(session);
|
|
1034
1039
|
this.injectDetectedEnvironmentMcp(session);
|
|
1035
1040
|
this.agentMcpBridge?.onSessionReady(session.id, backendType, session.state.cwd);
|
|
1041
|
+
this.collectiveIntelligence?.warmForSession(session.state.repo_root || session.state.cwd || "", backendType);
|
|
1036
1042
|
} else if (msg.type === "session_update") {
|
|
1037
1043
|
session.state = { ...session.state, ...msg.session, backend_type: backendType };
|
|
1038
1044
|
this.refreshGitInfo(session, { notifyPoller: true });
|
|
@@ -1054,6 +1060,8 @@ export class WsBridge {
|
|
|
1054
1060
|
}
|
|
1055
1061
|
session.messageHistory.push(msg);
|
|
1056
1062
|
this.persistSession(session);
|
|
1063
|
+
// §3.4 trigger 1 (adapter result path): consolidate on turn boundary
|
|
1064
|
+
this.maybeConsolidateOnTurn(session);
|
|
1057
1065
|
}
|
|
1058
1066
|
|
|
1059
1067
|
// Handle permission requests
|
|
@@ -1375,6 +1383,7 @@ export class WsBridge {
|
|
|
1375
1383
|
this.persistSession(session);
|
|
1376
1384
|
this.injectDetectedEnvironmentMcp(session);
|
|
1377
1385
|
this.agentMcpBridge?.onSessionReady(session.id, session.backendType, session.state.cwd);
|
|
1386
|
+
this.collectiveIntelligence?.warmForSession(session.state.repo_root || session.state.cwd || "", session.backendType);
|
|
1378
1387
|
} else if (msg.subtype === "status") {
|
|
1379
1388
|
session.state.is_compacting = msg.status === "compacting";
|
|
1380
1389
|
|
|
@@ -1582,6 +1591,9 @@ export class WsBridge {
|
|
|
1582
1591
|
|
|
1583
1592
|
this.emitResultWebhooks(session, msg);
|
|
1584
1593
|
|
|
1594
|
+
// §3.4 trigger 1: consolidate on turn boundary (fire-and-forget)
|
|
1595
|
+
this.maybeConsolidateOnTurn(session);
|
|
1596
|
+
|
|
1585
1597
|
// Trigger auto-naming after the first successful result
|
|
1586
1598
|
this.tryAutoNaming(session, browserMsg);
|
|
1587
1599
|
}
|
|
@@ -1690,21 +1702,31 @@ export class WsBridge {
|
|
|
1690
1702
|
return true;
|
|
1691
1703
|
}
|
|
1692
1704
|
|
|
1693
|
-
/**
|
|
1705
|
+
/** Append a user message to session history with a stable id for reconnect dedup. */
|
|
1706
|
+
private pushUserHistoryEntry(session: Session, content: string): string {
|
|
1707
|
+
const ts = Date.now();
|
|
1708
|
+
const id = `user-${ts}-${this.userMsgCounter++}`;
|
|
1709
|
+
session.messageHistory.push({ type: "user_message", content, timestamp: ts, id });
|
|
1710
|
+
return id;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Route a browser message to an adapter-based backend (Codex, Goose, etc.).
|
|
1715
|
+
* For user_message, `userHistoryContent` (when set) is what goes into
|
|
1716
|
+
* messageHistory — the original user text — while `msg.content` (possibly
|
|
1717
|
+
* memory-enriched, §3.6.1) is what the adapter receives. Returns the
|
|
1718
|
+
* history entry id for user messages.
|
|
1719
|
+
*/
|
|
1694
1720
|
private routeToAdapter(
|
|
1695
1721
|
session: Session,
|
|
1696
1722
|
msg: BrowserOutgoingMessage,
|
|
1697
1723
|
ws?: ServerWebSocket<SocketData>,
|
|
1698
|
-
|
|
1724
|
+
userHistoryContent?: string,
|
|
1725
|
+
): string | null {
|
|
1726
|
+
let userHistoryId: string | null = null;
|
|
1699
1727
|
// Store user messages in history for replay with stable ID for dedup on reconnect
|
|
1700
1728
|
if (msg.type === "user_message") {
|
|
1701
|
-
|
|
1702
|
-
session.messageHistory.push({
|
|
1703
|
-
type: "user_message",
|
|
1704
|
-
content: msg.content,
|
|
1705
|
-
timestamp: ts,
|
|
1706
|
-
id: `user-${ts}-${this.userMsgCounter++}`,
|
|
1707
|
-
});
|
|
1729
|
+
userHistoryId = this.pushUserHistoryEntry(session, userHistoryContent ?? msg.content);
|
|
1708
1730
|
this.persistSession(session);
|
|
1709
1731
|
}
|
|
1710
1732
|
|
|
@@ -1714,7 +1736,7 @@ export class WsBridge {
|
|
|
1714
1736
|
const eligibleVoters = this.countEligibleVoters(session);
|
|
1715
1737
|
if (eligibleVoters > 1) {
|
|
1716
1738
|
this.recordVote(session, msg.request_id, ws, msg.behavior, msg);
|
|
1717
|
-
return;
|
|
1739
|
+
return null;
|
|
1718
1740
|
}
|
|
1719
1741
|
const pending = session.pendingPermissions.get(msg.request_id);
|
|
1720
1742
|
if (session.backendType === "claude" && msg.behavior === "allow" && !msg.updated_input && pending) {
|
|
@@ -1731,18 +1753,24 @@ export class WsBridge {
|
|
|
1731
1753
|
console.log(`[ws-bridge] ${session.backendType} adapter not yet attached for session ${session.id}, queuing ${msg.type}`);
|
|
1732
1754
|
session.pendingMessages.push(JSON.stringify(outboundMsg));
|
|
1733
1755
|
}
|
|
1756
|
+
return userHistoryId;
|
|
1734
1757
|
}
|
|
1735
1758
|
|
|
1736
|
-
/**
|
|
1759
|
+
/**
|
|
1760
|
+
* Route a browser message to the Claude Code CLI backend.
|
|
1761
|
+
* `userHistoryContent` behaves as in routeToAdapter (§3.6.1): history text
|
|
1762
|
+
* override for user messages. Returns the history entry id for user
|
|
1763
|
+
* messages, null otherwise.
|
|
1764
|
+
*/
|
|
1737
1765
|
private routeToClaude(
|
|
1738
1766
|
session: Session,
|
|
1739
1767
|
msg: BrowserOutgoingMessage,
|
|
1740
1768
|
ws?: ServerWebSocket<SocketData>,
|
|
1741
|
-
|
|
1769
|
+
userHistoryContent?: string,
|
|
1770
|
+
): string | null {
|
|
1742
1771
|
switch (msg.type) {
|
|
1743
1772
|
case "user_message":
|
|
1744
|
-
this.handleUserMessage(session, msg);
|
|
1745
|
-
break;
|
|
1773
|
+
return this.handleUserMessage(session, msg, userHistoryContent);
|
|
1746
1774
|
case "permission_response":
|
|
1747
1775
|
this.handlePermissionResponse(session, msg, ws);
|
|
1748
1776
|
break;
|
|
@@ -1768,6 +1796,7 @@ export class WsBridge {
|
|
|
1768
1796
|
this.handleMcpSetServers(session, msg.servers);
|
|
1769
1797
|
break;
|
|
1770
1798
|
}
|
|
1799
|
+
return null;
|
|
1771
1800
|
}
|
|
1772
1801
|
|
|
1773
1802
|
private routeBrowserMessage(
|
|
@@ -1819,15 +1848,167 @@ export class WsBridge {
|
|
|
1819
1848
|
this.rememberClientMessage(session, msg.client_msg_id);
|
|
1820
1849
|
}
|
|
1821
1850
|
|
|
1851
|
+
// §3.6.1: user messages take the (possibly async) memory-enrichment path
|
|
1852
|
+
// after all RBAC / idempotency gates above. Everything else dispatches
|
|
1853
|
+
// synchronously exactly as before.
|
|
1854
|
+
if (msg.type === "user_message") {
|
|
1855
|
+
this.handleUserMessageWithEnrichment(session, msg, ws);
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
this.dispatchBrowserMessage(session, msg, ws);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* Final backend dispatch (adapter vs Claude CLI), extracted from
|
|
1864
|
+
* routeBrowserMessage so the enrichment path can reuse it.
|
|
1865
|
+
*
|
|
1866
|
+
* For user_message, `userHistoryContent` overrides the text stored in
|
|
1867
|
+
* messageHistory (history shows what the user typed; `msg.content` — which
|
|
1868
|
+
* may carry the enrichment block — goes to the backend). Returns the
|
|
1869
|
+
* history entry id for user messages, null otherwise.
|
|
1870
|
+
*/
|
|
1871
|
+
private dispatchBrowserMessage(
|
|
1872
|
+
session: Session,
|
|
1873
|
+
msg: BrowserOutgoingMessage,
|
|
1874
|
+
ws?: ServerWebSocket<SocketData>,
|
|
1875
|
+
userHistoryContent?: string,
|
|
1876
|
+
): string | null {
|
|
1822
1877
|
if (session.adapter) {
|
|
1823
|
-
this.routeToAdapter(session, msg, ws);
|
|
1878
|
+
return this.routeToAdapter(session, msg, ws, userHistoryContent);
|
|
1824
1879
|
} else if (session.backendType === "claude") {
|
|
1825
|
-
this.routeToClaude(session, msg, ws);
|
|
1880
|
+
return this.routeToClaude(session, msg, ws, userHistoryContent);
|
|
1826
1881
|
} else {
|
|
1827
|
-
this.routeToAdapter(session, msg, ws);
|
|
1882
|
+
return this.routeToAdapter(session, msg, ws, userHistoryContent);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* user_message routing with the memory-enrichment hook (§3.6.1–3.6.4).
|
|
1888
|
+
*
|
|
1889
|
+
* ASYNC-RIPPLE DECISION: routeBrowserMessage stays synchronous. Enrichment
|
|
1890
|
+
* is an async pre-step performed only for user_message and only when a
|
|
1891
|
+
* collective-intelligence layer is attached — so all existing synchronous
|
|
1892
|
+
* callers (handleBrowserMessage, injectUserMessage, setMcpServers,
|
|
1893
|
+
* injectDetectedEnvironmentMcp) keep their contracts, and sessions without
|
|
1894
|
+
* CI keep the fully synchronous v1 path. Because each enrichment awaits
|
|
1895
|
+
* independently, two rapid user messages could otherwise dispatch out of
|
|
1896
|
+
* order (a slow enrichment on msg1 racing a fast one on msg2); to preserve
|
|
1897
|
+
* per-session ordering we serialize user-message dispatch through a
|
|
1898
|
+
* per-session promise chain. The chain can back up at most
|
|
1899
|
+
* ENRICHMENT_TIMEOUT_MS per message, so chat never blocks on memory.
|
|
1900
|
+
*/
|
|
1901
|
+
private handleUserMessageWithEnrichment(
|
|
1902
|
+
session: Session,
|
|
1903
|
+
msg: BrowserOutgoingMessage & { type: "user_message" },
|
|
1904
|
+
ws?: ServerWebSocket<SocketData>,
|
|
1905
|
+
): void {
|
|
1906
|
+
// §3.4 idle trigger: any user activity resets the idle clock.
|
|
1907
|
+
try {
|
|
1908
|
+
noteSessionActivity(this.consolidationContext(session));
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
console.warn(`[ws-bridge] noteSessionActivity failed for ${session.id}:`, err);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
const ci = this.collectiveIntelligence;
|
|
1914
|
+
if (!ci || typeof ci.enrichUserMessage !== "function") {
|
|
1915
|
+
// No CI layer attached — keep the fully synchronous dispatch path.
|
|
1916
|
+
this.dispatchBrowserMessage(session, msg, ws);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const prior = this.userMessageChains.get(session.id) ?? Promise.resolve();
|
|
1921
|
+
const next = prior.then(() =>
|
|
1922
|
+
this.enrichAndDispatchUserMessage(session, msg, ws).catch((err) => {
|
|
1923
|
+
console.warn(`[ws-bridge] user_message dispatch failed for ${session.id}:`, err);
|
|
1924
|
+
}),
|
|
1925
|
+
);
|
|
1926
|
+
this.userMessageChains.set(session.id, next);
|
|
1927
|
+
// Drop the chain entry once fully drained so the map doesn't grow forever.
|
|
1928
|
+
void next.finally(() => {
|
|
1929
|
+
if (this.userMessageChains.get(session.id) === next) {
|
|
1930
|
+
this.userMessageChains.delete(session.id);
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/**
|
|
1936
|
+
* Enrich a user message with recalled memory, dispatch it, and surface the
|
|
1937
|
+
* recall to browsers (§3.6.1–3.6.4):
|
|
1938
|
+
* - enrichment is raced against ENRICHMENT_TIMEOUT_MS and try/caught —
|
|
1939
|
+
* on timeout or error the ORIGINAL content is sent unchanged;
|
|
1940
|
+
* - the backend receives `<block>\n\n<original>`, while messageHistory
|
|
1941
|
+
* stores the original user text;
|
|
1942
|
+
* - after the history entry exists, a memory_enriched message carrying its
|
|
1943
|
+
* id + the recalled items is broadcast (skipped when block is null).
|
|
1944
|
+
*/
|
|
1945
|
+
private async enrichAndDispatchUserMessage(
|
|
1946
|
+
session: Session,
|
|
1947
|
+
msg: BrowserOutgoingMessage & { type: "user_message" },
|
|
1948
|
+
ws?: ServerWebSocket<SocketData>,
|
|
1949
|
+
): Promise<void> {
|
|
1950
|
+
let enrichment: import("./semantic-memory.js").EnrichmentResult | null = null;
|
|
1951
|
+
try {
|
|
1952
|
+
const timeout = new Promise<null>((resolveRace) => {
|
|
1953
|
+
const timer = setTimeout(() => resolveRace(null), WsBridge.ENRICHMENT_TIMEOUT_MS);
|
|
1954
|
+
timer.unref?.();
|
|
1955
|
+
});
|
|
1956
|
+
enrichment = await Promise.race([
|
|
1957
|
+
this.collectiveIntelligence!.enrichUserMessage(this.consolidationContext(session), msg.content),
|
|
1958
|
+
timeout,
|
|
1959
|
+
]);
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
console.warn(`[ws-bridge] memory enrichment failed for ${session.id} (sending original):`, err);
|
|
1962
|
+
enrichment = null;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
const block = enrichment?.block ?? null;
|
|
1966
|
+
const outbound = block ? { ...msg, content: `${block}\n\n${msg.content}` } : msg;
|
|
1967
|
+
const historyId = this.dispatchBrowserMessage(session, outbound, ws, msg.content);
|
|
1968
|
+
|
|
1969
|
+
if (block && enrichment && enrichment.items.length > 0) {
|
|
1970
|
+
this.broadcastToBrowsers(session, {
|
|
1971
|
+
type: "memory_enriched",
|
|
1972
|
+
user_message_id: historyId ?? undefined,
|
|
1973
|
+
items: enrichment.items,
|
|
1974
|
+
});
|
|
1828
1975
|
}
|
|
1829
1976
|
}
|
|
1830
1977
|
|
|
1978
|
+
/** Session context for memory consolidation / enrichment (§3.6 item 5). */
|
|
1979
|
+
private consolidationContext(session: Session): {
|
|
1980
|
+
sessionId: string;
|
|
1981
|
+
repoRoot: string;
|
|
1982
|
+
backendType: BackendType;
|
|
1983
|
+
} {
|
|
1984
|
+
return {
|
|
1985
|
+
sessionId: session.id,
|
|
1986
|
+
repoRoot: session.state.repo_root || session.state.cwd || "",
|
|
1987
|
+
backendType: session.backendType,
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* §3.4 turn-boundary trigger: on every result message, record activity for
|
|
1993
|
+
* the idle watcher and — fire-and-forget, off the hot path — consolidate
|
|
1994
|
+
* when the un-consolidated fragment threshold is met.
|
|
1995
|
+
*/
|
|
1996
|
+
private maybeConsolidateOnTurn(session: Session): void {
|
|
1997
|
+
const ctx = this.consolidationContext(session);
|
|
1998
|
+
try {
|
|
1999
|
+
noteSessionActivity(ctx);
|
|
2000
|
+
} catch (err) {
|
|
2001
|
+
console.warn(`[ws-bridge] noteSessionActivity failed for ${session.id}:`, err);
|
|
2002
|
+
}
|
|
2003
|
+
void (async () => {
|
|
2004
|
+
if (await shouldConsolidateOnTurn(session.id)) {
|
|
2005
|
+
await consolidate({ ...ctx, reason: "turn_boundary" });
|
|
2006
|
+
}
|
|
2007
|
+
})().catch((err) => {
|
|
2008
|
+
console.warn(`[ws-bridge] turn-boundary consolidation failed for ${session.id}:`, err);
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1831
2012
|
private isDuplicateClientMessage(session: Session, clientMsgId: string): boolean {
|
|
1832
2013
|
return session.processedClientMessageIdSet.has(clientMsgId);
|
|
1833
2014
|
}
|
|
@@ -1902,18 +2083,19 @@ export class WsBridge {
|
|
|
1902
2083
|
}
|
|
1903
2084
|
}
|
|
1904
2085
|
|
|
2086
|
+
/**
|
|
2087
|
+
* Send a user message to the Claude CLI. `historyContent` (when set) is the
|
|
2088
|
+
* text stored in messageHistory — the original user prompt — while
|
|
2089
|
+
* `msg.content` (possibly memory-enriched, §3.6.1) goes to the CLI.
|
|
2090
|
+
* Returns the history entry id.
|
|
2091
|
+
*/
|
|
1905
2092
|
private handleUserMessage(
|
|
1906
2093
|
session: Session,
|
|
1907
|
-
msg: { type: "user_message"; content: string; session_id?: string; images?: { media_type: string; data: string }[] }
|
|
1908
|
-
|
|
2094
|
+
msg: { type: "user_message"; content: string; session_id?: string; images?: { media_type: string; data: string }[] },
|
|
2095
|
+
historyContent?: string,
|
|
2096
|
+
): string {
|
|
1909
2097
|
// Store user message in history for replay with stable ID for dedup on reconnect
|
|
1910
|
-
const
|
|
1911
|
-
session.messageHistory.push({
|
|
1912
|
-
type: "user_message",
|
|
1913
|
-
content: msg.content,
|
|
1914
|
-
timestamp: ts,
|
|
1915
|
-
id: `user-${ts}-${this.userMsgCounter++}`,
|
|
1916
|
-
});
|
|
2098
|
+
const historyId = this.pushUserHistoryEntry(session, historyContent ?? msg.content);
|
|
1917
2099
|
|
|
1918
2100
|
// Build content: if images are present, use content block array; otherwise plain string
|
|
1919
2101
|
let content: string | unknown[];
|
|
@@ -1939,6 +2121,7 @@ export class WsBridge {
|
|
|
1939
2121
|
});
|
|
1940
2122
|
this.sendToCLI(session, ndjson);
|
|
1941
2123
|
this.persistSession(session);
|
|
2124
|
+
return historyId;
|
|
1942
2125
|
}
|
|
1943
2126
|
|
|
1944
2127
|
private handlePermissionResponse(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ap as o,aq as n}from"./mermaid.core-Z9YQUdi5.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-DGvVY1Y_.js";import{_ as i}from"./mermaid.core-Z9YQUdi5.js";import"./chunk-FMBD7UC4-Ctqs0bWj.js";import"./chunk-55IACEB6-BbI83gIK.js";import"./chunk-QN33PNHL-CfvfTLi4.js";import"./index-CAGJQXdJ.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-DGvVY1Y_.js";import{_ as i}from"./mermaid.core-Z9YQUdi5.js";import"./chunk-FMBD7UC4-Ctqs0bWj.js";import"./chunk-55IACEB6-BbI83gIK.js";import"./chunk-QN33PNHL-CfvfTLi4.js";import"./index-CAGJQXdJ.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as r}from"./_baseUniq-CZ0BR5Mx.js";var e=4;function a(o){return r(o,e)}export{a as c};
|