the-campfire 0.2.1 → 0.3.0
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 +125 -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 +1127 -187
- package/server/session-types.ts +15 -0
- package/server/settings-manager.ts +122 -3
- package/server/ws-bridge.ts +207 -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",
|
|
@@ -1054,6 +1059,8 @@ export class WsBridge {
|
|
|
1054
1059
|
}
|
|
1055
1060
|
session.messageHistory.push(msg);
|
|
1056
1061
|
this.persistSession(session);
|
|
1062
|
+
// §3.4 trigger 1 (adapter result path): consolidate on turn boundary
|
|
1063
|
+
this.maybeConsolidateOnTurn(session);
|
|
1057
1064
|
}
|
|
1058
1065
|
|
|
1059
1066
|
// Handle permission requests
|
|
@@ -1582,6 +1589,9 @@ export class WsBridge {
|
|
|
1582
1589
|
|
|
1583
1590
|
this.emitResultWebhooks(session, msg);
|
|
1584
1591
|
|
|
1592
|
+
// §3.4 trigger 1: consolidate on turn boundary (fire-and-forget)
|
|
1593
|
+
this.maybeConsolidateOnTurn(session);
|
|
1594
|
+
|
|
1585
1595
|
// Trigger auto-naming after the first successful result
|
|
1586
1596
|
this.tryAutoNaming(session, browserMsg);
|
|
1587
1597
|
}
|
|
@@ -1690,21 +1700,31 @@ export class WsBridge {
|
|
|
1690
1700
|
return true;
|
|
1691
1701
|
}
|
|
1692
1702
|
|
|
1693
|
-
/**
|
|
1703
|
+
/** Append a user message to session history with a stable id for reconnect dedup. */
|
|
1704
|
+
private pushUserHistoryEntry(session: Session, content: string): string {
|
|
1705
|
+
const ts = Date.now();
|
|
1706
|
+
const id = `user-${ts}-${this.userMsgCounter++}`;
|
|
1707
|
+
session.messageHistory.push({ type: "user_message", content, timestamp: ts, id });
|
|
1708
|
+
return id;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Route a browser message to an adapter-based backend (Codex, Goose, etc.).
|
|
1713
|
+
* For user_message, `userHistoryContent` (when set) is what goes into
|
|
1714
|
+
* messageHistory — the original user text — while `msg.content` (possibly
|
|
1715
|
+
* memory-enriched, §3.6.1) is what the adapter receives. Returns the
|
|
1716
|
+
* history entry id for user messages.
|
|
1717
|
+
*/
|
|
1694
1718
|
private routeToAdapter(
|
|
1695
1719
|
session: Session,
|
|
1696
1720
|
msg: BrowserOutgoingMessage,
|
|
1697
1721
|
ws?: ServerWebSocket<SocketData>,
|
|
1698
|
-
|
|
1722
|
+
userHistoryContent?: string,
|
|
1723
|
+
): string | null {
|
|
1724
|
+
let userHistoryId: string | null = null;
|
|
1699
1725
|
// Store user messages in history for replay with stable ID for dedup on reconnect
|
|
1700
1726
|
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
|
-
});
|
|
1727
|
+
userHistoryId = this.pushUserHistoryEntry(session, userHistoryContent ?? msg.content);
|
|
1708
1728
|
this.persistSession(session);
|
|
1709
1729
|
}
|
|
1710
1730
|
|
|
@@ -1714,7 +1734,7 @@ export class WsBridge {
|
|
|
1714
1734
|
const eligibleVoters = this.countEligibleVoters(session);
|
|
1715
1735
|
if (eligibleVoters > 1) {
|
|
1716
1736
|
this.recordVote(session, msg.request_id, ws, msg.behavior, msg);
|
|
1717
|
-
return;
|
|
1737
|
+
return null;
|
|
1718
1738
|
}
|
|
1719
1739
|
const pending = session.pendingPermissions.get(msg.request_id);
|
|
1720
1740
|
if (session.backendType === "claude" && msg.behavior === "allow" && !msg.updated_input && pending) {
|
|
@@ -1731,18 +1751,24 @@ export class WsBridge {
|
|
|
1731
1751
|
console.log(`[ws-bridge] ${session.backendType} adapter not yet attached for session ${session.id}, queuing ${msg.type}`);
|
|
1732
1752
|
session.pendingMessages.push(JSON.stringify(outboundMsg));
|
|
1733
1753
|
}
|
|
1754
|
+
return userHistoryId;
|
|
1734
1755
|
}
|
|
1735
1756
|
|
|
1736
|
-
/**
|
|
1757
|
+
/**
|
|
1758
|
+
* Route a browser message to the Claude Code CLI backend.
|
|
1759
|
+
* `userHistoryContent` behaves as in routeToAdapter (§3.6.1): history text
|
|
1760
|
+
* override for user messages. Returns the history entry id for user
|
|
1761
|
+
* messages, null otherwise.
|
|
1762
|
+
*/
|
|
1737
1763
|
private routeToClaude(
|
|
1738
1764
|
session: Session,
|
|
1739
1765
|
msg: BrowserOutgoingMessage,
|
|
1740
1766
|
ws?: ServerWebSocket<SocketData>,
|
|
1741
|
-
|
|
1767
|
+
userHistoryContent?: string,
|
|
1768
|
+
): string | null {
|
|
1742
1769
|
switch (msg.type) {
|
|
1743
1770
|
case "user_message":
|
|
1744
|
-
this.handleUserMessage(session, msg);
|
|
1745
|
-
break;
|
|
1771
|
+
return this.handleUserMessage(session, msg, userHistoryContent);
|
|
1746
1772
|
case "permission_response":
|
|
1747
1773
|
this.handlePermissionResponse(session, msg, ws);
|
|
1748
1774
|
break;
|
|
@@ -1768,6 +1794,7 @@ export class WsBridge {
|
|
|
1768
1794
|
this.handleMcpSetServers(session, msg.servers);
|
|
1769
1795
|
break;
|
|
1770
1796
|
}
|
|
1797
|
+
return null;
|
|
1771
1798
|
}
|
|
1772
1799
|
|
|
1773
1800
|
private routeBrowserMessage(
|
|
@@ -1819,15 +1846,167 @@ export class WsBridge {
|
|
|
1819
1846
|
this.rememberClientMessage(session, msg.client_msg_id);
|
|
1820
1847
|
}
|
|
1821
1848
|
|
|
1849
|
+
// §3.6.1: user messages take the (possibly async) memory-enrichment path
|
|
1850
|
+
// after all RBAC / idempotency gates above. Everything else dispatches
|
|
1851
|
+
// synchronously exactly as before.
|
|
1852
|
+
if (msg.type === "user_message") {
|
|
1853
|
+
this.handleUserMessageWithEnrichment(session, msg, ws);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
this.dispatchBrowserMessage(session, msg, ws);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Final backend dispatch (adapter vs Claude CLI), extracted from
|
|
1862
|
+
* routeBrowserMessage so the enrichment path can reuse it.
|
|
1863
|
+
*
|
|
1864
|
+
* For user_message, `userHistoryContent` overrides the text stored in
|
|
1865
|
+
* messageHistory (history shows what the user typed; `msg.content` — which
|
|
1866
|
+
* may carry the enrichment block — goes to the backend). Returns the
|
|
1867
|
+
* history entry id for user messages, null otherwise.
|
|
1868
|
+
*/
|
|
1869
|
+
private dispatchBrowserMessage(
|
|
1870
|
+
session: Session,
|
|
1871
|
+
msg: BrowserOutgoingMessage,
|
|
1872
|
+
ws?: ServerWebSocket<SocketData>,
|
|
1873
|
+
userHistoryContent?: string,
|
|
1874
|
+
): string | null {
|
|
1822
1875
|
if (session.adapter) {
|
|
1823
|
-
this.routeToAdapter(session, msg, ws);
|
|
1876
|
+
return this.routeToAdapter(session, msg, ws, userHistoryContent);
|
|
1824
1877
|
} else if (session.backendType === "claude") {
|
|
1825
|
-
this.routeToClaude(session, msg, ws);
|
|
1878
|
+
return this.routeToClaude(session, msg, ws, userHistoryContent);
|
|
1826
1879
|
} else {
|
|
1827
|
-
this.routeToAdapter(session, msg, ws);
|
|
1880
|
+
return this.routeToAdapter(session, msg, ws, userHistoryContent);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/**
|
|
1885
|
+
* user_message routing with the memory-enrichment hook (§3.6.1–3.6.4).
|
|
1886
|
+
*
|
|
1887
|
+
* ASYNC-RIPPLE DECISION: routeBrowserMessage stays synchronous. Enrichment
|
|
1888
|
+
* is an async pre-step performed only for user_message and only when a
|
|
1889
|
+
* collective-intelligence layer is attached — so all existing synchronous
|
|
1890
|
+
* callers (handleBrowserMessage, injectUserMessage, setMcpServers,
|
|
1891
|
+
* injectDetectedEnvironmentMcp) keep their contracts, and sessions without
|
|
1892
|
+
* CI keep the fully synchronous v1 path. Because each enrichment awaits
|
|
1893
|
+
* independently, two rapid user messages could otherwise dispatch out of
|
|
1894
|
+
* order (a slow enrichment on msg1 racing a fast one on msg2); to preserve
|
|
1895
|
+
* per-session ordering we serialize user-message dispatch through a
|
|
1896
|
+
* per-session promise chain. The chain can back up at most
|
|
1897
|
+
* ENRICHMENT_TIMEOUT_MS per message, so chat never blocks on memory.
|
|
1898
|
+
*/
|
|
1899
|
+
private handleUserMessageWithEnrichment(
|
|
1900
|
+
session: Session,
|
|
1901
|
+
msg: BrowserOutgoingMessage & { type: "user_message" },
|
|
1902
|
+
ws?: ServerWebSocket<SocketData>,
|
|
1903
|
+
): void {
|
|
1904
|
+
// §3.4 idle trigger: any user activity resets the idle clock.
|
|
1905
|
+
try {
|
|
1906
|
+
noteSessionActivity(this.consolidationContext(session));
|
|
1907
|
+
} catch (err) {
|
|
1908
|
+
console.warn(`[ws-bridge] noteSessionActivity failed for ${session.id}:`, err);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const ci = this.collectiveIntelligence;
|
|
1912
|
+
if (!ci || typeof ci.enrichUserMessage !== "function") {
|
|
1913
|
+
// No CI layer attached — keep the fully synchronous dispatch path.
|
|
1914
|
+
this.dispatchBrowserMessage(session, msg, ws);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const prior = this.userMessageChains.get(session.id) ?? Promise.resolve();
|
|
1919
|
+
const next = prior.then(() =>
|
|
1920
|
+
this.enrichAndDispatchUserMessage(session, msg, ws).catch((err) => {
|
|
1921
|
+
console.warn(`[ws-bridge] user_message dispatch failed for ${session.id}:`, err);
|
|
1922
|
+
}),
|
|
1923
|
+
);
|
|
1924
|
+
this.userMessageChains.set(session.id, next);
|
|
1925
|
+
// Drop the chain entry once fully drained so the map doesn't grow forever.
|
|
1926
|
+
void next.finally(() => {
|
|
1927
|
+
if (this.userMessageChains.get(session.id) === next) {
|
|
1928
|
+
this.userMessageChains.delete(session.id);
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* Enrich a user message with recalled memory, dispatch it, and surface the
|
|
1935
|
+
* recall to browsers (§3.6.1–3.6.4):
|
|
1936
|
+
* - enrichment is raced against ENRICHMENT_TIMEOUT_MS and try/caught —
|
|
1937
|
+
* on timeout or error the ORIGINAL content is sent unchanged;
|
|
1938
|
+
* - the backend receives `<block>\n\n<original>`, while messageHistory
|
|
1939
|
+
* stores the original user text;
|
|
1940
|
+
* - after the history entry exists, a memory_enriched message carrying its
|
|
1941
|
+
* id + the recalled items is broadcast (skipped when block is null).
|
|
1942
|
+
*/
|
|
1943
|
+
private async enrichAndDispatchUserMessage(
|
|
1944
|
+
session: Session,
|
|
1945
|
+
msg: BrowserOutgoingMessage & { type: "user_message" },
|
|
1946
|
+
ws?: ServerWebSocket<SocketData>,
|
|
1947
|
+
): Promise<void> {
|
|
1948
|
+
let enrichment: import("./semantic-memory.js").EnrichmentResult | null = null;
|
|
1949
|
+
try {
|
|
1950
|
+
const timeout = new Promise<null>((resolveRace) => {
|
|
1951
|
+
const timer = setTimeout(() => resolveRace(null), WsBridge.ENRICHMENT_TIMEOUT_MS);
|
|
1952
|
+
timer.unref?.();
|
|
1953
|
+
});
|
|
1954
|
+
enrichment = await Promise.race([
|
|
1955
|
+
this.collectiveIntelligence!.enrichUserMessage(this.consolidationContext(session), msg.content),
|
|
1956
|
+
timeout,
|
|
1957
|
+
]);
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
console.warn(`[ws-bridge] memory enrichment failed for ${session.id} (sending original):`, err);
|
|
1960
|
+
enrichment = null;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
const block = enrichment?.block ?? null;
|
|
1964
|
+
const outbound = block ? { ...msg, content: `${block}\n\n${msg.content}` } : msg;
|
|
1965
|
+
const historyId = this.dispatchBrowserMessage(session, outbound, ws, msg.content);
|
|
1966
|
+
|
|
1967
|
+
if (block && enrichment && enrichment.items.length > 0) {
|
|
1968
|
+
this.broadcastToBrowsers(session, {
|
|
1969
|
+
type: "memory_enriched",
|
|
1970
|
+
user_message_id: historyId ?? undefined,
|
|
1971
|
+
items: enrichment.items,
|
|
1972
|
+
});
|
|
1828
1973
|
}
|
|
1829
1974
|
}
|
|
1830
1975
|
|
|
1976
|
+
/** Session context for memory consolidation / enrichment (§3.6 item 5). */
|
|
1977
|
+
private consolidationContext(session: Session): {
|
|
1978
|
+
sessionId: string;
|
|
1979
|
+
repoRoot: string;
|
|
1980
|
+
backendType: BackendType;
|
|
1981
|
+
} {
|
|
1982
|
+
return {
|
|
1983
|
+
sessionId: session.id,
|
|
1984
|
+
repoRoot: session.state.repo_root || session.state.cwd || "",
|
|
1985
|
+
backendType: session.backendType,
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* §3.4 turn-boundary trigger: on every result message, record activity for
|
|
1991
|
+
* the idle watcher and — fire-and-forget, off the hot path — consolidate
|
|
1992
|
+
* when the un-consolidated fragment threshold is met.
|
|
1993
|
+
*/
|
|
1994
|
+
private maybeConsolidateOnTurn(session: Session): void {
|
|
1995
|
+
const ctx = this.consolidationContext(session);
|
|
1996
|
+
try {
|
|
1997
|
+
noteSessionActivity(ctx);
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
console.warn(`[ws-bridge] noteSessionActivity failed for ${session.id}:`, err);
|
|
2000
|
+
}
|
|
2001
|
+
void (async () => {
|
|
2002
|
+
if (await shouldConsolidateOnTurn(session.id)) {
|
|
2003
|
+
await consolidate({ ...ctx, reason: "turn_boundary" });
|
|
2004
|
+
}
|
|
2005
|
+
})().catch((err) => {
|
|
2006
|
+
console.warn(`[ws-bridge] turn-boundary consolidation failed for ${session.id}:`, err);
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1831
2010
|
private isDuplicateClientMessage(session: Session, clientMsgId: string): boolean {
|
|
1832
2011
|
return session.processedClientMessageIdSet.has(clientMsgId);
|
|
1833
2012
|
}
|
|
@@ -1902,18 +2081,19 @@ export class WsBridge {
|
|
|
1902
2081
|
}
|
|
1903
2082
|
}
|
|
1904
2083
|
|
|
2084
|
+
/**
|
|
2085
|
+
* Send a user message to the Claude CLI. `historyContent` (when set) is the
|
|
2086
|
+
* text stored in messageHistory — the original user prompt — while
|
|
2087
|
+
* `msg.content` (possibly memory-enriched, §3.6.1) goes to the CLI.
|
|
2088
|
+
* Returns the history entry id.
|
|
2089
|
+
*/
|
|
1905
2090
|
private handleUserMessage(
|
|
1906
2091
|
session: Session,
|
|
1907
|
-
msg: { type: "user_message"; content: string; session_id?: string; images?: { media_type: string; data: string }[] }
|
|
1908
|
-
|
|
2092
|
+
msg: { type: "user_message"; content: string; session_id?: string; images?: { media_type: string; data: string }[] },
|
|
2093
|
+
historyContent?: string,
|
|
2094
|
+
): string {
|
|
1909
2095
|
// 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
|
-
});
|
|
2096
|
+
const historyId = this.pushUserHistoryEntry(session, historyContent ?? msg.content);
|
|
1917
2097
|
|
|
1918
2098
|
// Build content: if images are present, use content block array; otherwise plain string
|
|
1919
2099
|
let content: string | unknown[];
|
|
@@ -1939,6 +2119,7 @@ export class WsBridge {
|
|
|
1939
2119
|
});
|
|
1940
2120
|
this.sendToCLI(session, ndjson);
|
|
1941
2121
|
this.persistSession(session);
|
|
2122
|
+
return historyId;
|
|
1942
2123
|
}
|
|
1943
2124
|
|
|
1944
2125
|
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};
|