@tom2012/cc-web 1.5.92 → 1.5.94

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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/backend/dist/memory-pool/global-pool-manager.d.ts +12 -0
  3. package/backend/dist/memory-pool/global-pool-manager.d.ts.map +1 -0
  4. package/backend/dist/memory-pool/global-pool-manager.js +290 -0
  5. package/backend/dist/memory-pool/global-pool-manager.js.map +1 -0
  6. package/backend/dist/memory-pool/pool-lock.d.ts +2 -0
  7. package/backend/dist/memory-pool/pool-lock.d.ts.map +1 -0
  8. package/backend/dist/memory-pool/pool-lock.js +31 -0
  9. package/backend/dist/memory-pool/pool-lock.js.map +1 -0
  10. package/backend/dist/memory-pool/pool-manager.d.ts +34 -0
  11. package/backend/dist/memory-pool/pool-manager.d.ts.map +1 -1
  12. package/backend/dist/memory-pool/pool-manager.js +91 -0
  13. package/backend/dist/memory-pool/pool-manager.js.map +1 -1
  14. package/backend/dist/memory-pool/templates.d.ts.map +1 -1
  15. package/backend/dist/memory-pool/templates.js +98 -127
  16. package/backend/dist/memory-pool/templates.js.map +1 -1
  17. package/backend/dist/memory-pool/types.d.ts +34 -0
  18. package/backend/dist/memory-pool/types.d.ts.map +1 -1
  19. package/backend/dist/routes/hooks.d.ts.map +1 -1
  20. package/backend/dist/routes/hooks.js +14 -0
  21. package/backend/dist/routes/hooks.js.map +1 -1
  22. package/backend/dist/routes/memory-pool.d.ts.map +1 -1
  23. package/backend/dist/routes/memory-pool.js +617 -57
  24. package/backend/dist/routes/memory-pool.js.map +1 -1
  25. package/frontend/dist/assets/{GraphPreview-QiSxy4LP.js → GraphPreview-BFYA_WA5.js} +1 -1
  26. package/frontend/dist/assets/{OfficePreview-DlUCjmFD.js → OfficePreview-DN-it4Af.js} +2 -2
  27. package/frontend/dist/assets/{PlanPanel-Dn5PCqCc.js → PlanPanel-DYupjf3X.js} +1 -1
  28. package/frontend/dist/assets/{ProjectPage-XG9MZm7t.js → ProjectPage-B6tVjIYr.js} +5 -5
  29. package/frontend/dist/assets/{SettingsPage-ofs1M1K_.js → SettingsPage-CQL_FfkR.js} +2 -2
  30. package/frontend/dist/assets/{ShareViewPage-u8FXEdhC.js → ShareViewPage-C_x31zAw.js} +1 -1
  31. package/frontend/dist/assets/{SkillHubPage-DYQ3--oW.js → SkillHubPage-Cgoxl30L.js} +2 -2
  32. package/frontend/dist/assets/{bot-63iGenXw.js → bot-Dst2XqWX.js} +1 -1
  33. package/frontend/dist/assets/{chevron-down-CzkwkeMW.js → chevron-down-DsAldQb3.js} +1 -1
  34. package/frontend/dist/assets/{download-DhFx0Fu-.js → download-YA6lXM4C.js} +1 -1
  35. package/frontend/dist/assets/{index-DVUjdhq8.css → index-BDYnq-Fr.css} +1 -1
  36. package/frontend/dist/assets/{index-C8aR4tRp.js → index-CY9Au1bg.js} +9 -9
  37. package/frontend/dist/assets/{index-DKsG2yPY.js → index-_m8u9esJ.js} +1 -1
  38. package/frontend/dist/assets/{jszip.min-B9vwwWDL.js → jszip.min-BC9RCrTS.js} +1 -1
  39. package/frontend/dist/assets/{matter-CCkX66fz.js → matter-Mm9BJmx6.js} +1 -1
  40. package/frontend/dist/assets/{maximize-2-B3mAuuFs.js → maximize-2-CuzznumM.js} +1 -1
  41. package/frontend/dist/assets/{save-CxQAMJ-_.js → save-Q2W6S7fk.js} +1 -1
  42. package/frontend/dist/assets/{user-DxwkzcYb.js → user-BM9GY1Xb.js} +1 -1
  43. package/frontend/dist/index.html +2 -2
  44. package/package.json +1 -1
@@ -39,8 +39,13 @@ const path = __importStar(require("path"));
39
39
  const config_1 = require("../config");
40
40
  const templates_1 = require("../memory-pool/templates");
41
41
  const pool_manager_1 = require("../memory-pool/pool-manager");
42
+ const buoyancy_1 = require("../memory-pool/buoyancy");
43
+ const pool_lock_1 = require("../memory-pool/pool-lock");
44
+ const global_pool_manager_1 = require("../memory-pool/global-pool-manager");
42
45
  const router = (0, express_1.Router)();
43
46
  const BALL_ID_RE = /^ball_\d{1,6}$/;
47
+ const VALID_TYPES = ['feedback', 'user', 'project', 'reference'];
48
+ const DEFAULT_B0 = { feedback: 9, user: 6, project: 5, reference: 3 };
44
49
  const MEMORY_POOL_DIR = '.memory-pool';
45
50
  const CLAUDE_MD_MARKER = '## 记忆池(Memory Pool)';
46
51
  function resolveProjectFolder(projectId, username, res) {
@@ -56,6 +61,164 @@ function resolveProjectFolder(projectId, username, res) {
56
61
  }
57
62
  return project.folderPath;
58
63
  }
64
+ function validateBallIds(ids) {
65
+ return ids.every((id) => typeof id === 'string' && BALL_ID_RE.test(id));
66
+ }
67
+ // ══════════════════════════════════════════════════════════════════════════════
68
+ // Global Memory Pool Endpoints (MUST be before /:projectId routes)
69
+ // ══════════════════════════════════════════════════════════════════════════════
70
+ // GET /api/memory-pool/global/status
71
+ router.get('/global/status', (_req, res) => {
72
+ if (!(0, global_pool_manager_1.isGlobalPoolInitialized)()) {
73
+ res.json({ initialized: false });
74
+ return;
75
+ }
76
+ const pool = (0, global_pool_manager_1.readGlobalPool)();
77
+ if (!pool) {
78
+ res.json({ initialized: false });
79
+ return;
80
+ }
81
+ const sources = (0, global_pool_manager_1.readSources)();
82
+ const freshT = (0, global_pool_manager_1.computeGlobalT)(pool.initialized_at);
83
+ res.json({
84
+ initialized: true,
85
+ state: {
86
+ version: pool.version,
87
+ t: freshT,
88
+ lambda: pool.lambda,
89
+ alpha: pool.alpha,
90
+ active_capacity: pool.active_capacity,
91
+ surface_width: pool.surface_width ?? 10000,
92
+ next_id: pool.next_id,
93
+ pool: pool.pool,
94
+ initialized_at: pool.initialized_at,
95
+ },
96
+ ballCount: pool.balls.length,
97
+ sourceCount: sources.sources.filter((s) => s.status === 'active').length,
98
+ activeBalls: Math.min(pool.balls.length, pool.active_capacity),
99
+ });
100
+ });
101
+ // GET /api/memory-pool/global/index
102
+ router.get('/global/index', (_req, res) => {
103
+ const pool = (0, global_pool_manager_1.readGlobalPool)();
104
+ if (!pool) {
105
+ res.status(404).json({ error: 'Global pool not initialized' });
106
+ return;
107
+ }
108
+ pool.t = (0, global_pool_manager_1.computeGlobalT)(pool.initialized_at);
109
+ const balls = (0, pool_manager_1.enrichBallsWithBuoyancy)(pool);
110
+ res.json({ t: pool.t, updated_at: new Date().toISOString(), balls, active_capacity: pool.active_capacity });
111
+ });
112
+ // GET /api/memory-pool/global/surface
113
+ router.get('/global/surface', (_req, res) => {
114
+ const globalDir = (0, global_pool_manager_1.getGlobalPoolDir)();
115
+ (0, pool_lock_1.withPoolLock)(globalDir, () => {
116
+ const pool = (0, global_pool_manager_1.readGlobalPool)();
117
+ if (!pool) {
118
+ res.status(404).json({ error: 'Global pool not initialized' });
119
+ return;
120
+ }
121
+ const freshT = (0, global_pool_manager_1.computeGlobalT)(pool.initialized_at);
122
+ if (pool.t !== freshT) {
123
+ pool.t = freshT;
124
+ (0, pool_manager_1.writePool)(globalDir, pool);
125
+ }
126
+ const { surfaceBalls, totalTokens } = (0, pool_manager_1.buildSurface)(globalDir);
127
+ res.json({
128
+ t: pool.t,
129
+ surface_width: pool.surface_width ?? 10000,
130
+ used_tokens: totalTokens,
131
+ balls: surfaceBalls,
132
+ });
133
+ }).catch((err) => {
134
+ if (!res.headersSent)
135
+ res.status(500).json({ error: err.message || err });
136
+ });
137
+ });
138
+ // GET /api/memory-pool/global/ball/:ballId (pure read, no side effect)
139
+ router.get('/global/ball/:ballId', (req, res) => {
140
+ const { ballId } = req.params;
141
+ if (!BALL_ID_RE.test(ballId)) {
142
+ res.status(400).json({ error: 'Invalid ball ID format' });
143
+ return;
144
+ }
145
+ const content = (0, pool_manager_1.readBallContent)((0, global_pool_manager_1.getGlobalPoolDir)(), ballId);
146
+ if (content === null) {
147
+ res.status(404).json({ error: 'Ball not found' });
148
+ return;
149
+ }
150
+ res.json({ id: ballId, content });
151
+ });
152
+ // POST /api/memory-pool/global/balls/:ballId/hit
153
+ router.post('/global/balls/:ballId/hit', (req, res) => {
154
+ const { ballId } = req.params;
155
+ if (!BALL_ID_RE.test(ballId)) {
156
+ res.status(400).json({ error: 'Invalid ball ID format' });
157
+ return;
158
+ }
159
+ const globalDir = (0, global_pool_manager_1.getGlobalPoolDir)();
160
+ (0, pool_lock_1.withPoolLock)(globalDir, () => {
161
+ const pool = (0, global_pool_manager_1.readGlobalPool)();
162
+ if (!pool) {
163
+ res.status(404).json({ error: 'Global pool not initialized' });
164
+ return;
165
+ }
166
+ pool.t = (0, global_pool_manager_1.computeGlobalT)(pool.initialized_at);
167
+ const ball = pool.balls.find((b) => b.id === ballId);
168
+ if (!ball) {
169
+ res.status(404).json({ error: 'Ball not found' });
170
+ return;
171
+ }
172
+ ball.H += 1;
173
+ ball.t_last = pool.t;
174
+ (0, pool_manager_1.writePool)(globalDir, pool);
175
+ const content = (0, pool_manager_1.readBallContent)(globalDir, ballId);
176
+ const buoy = (0, buoyancy_1.computeBuoyancy)(ball.B0, ball.H, pool.alpha, pool.lambda, pool.t, ball.t_last, ball.permanent);
177
+ const linkedBalls = ball.links
178
+ .map((lid) => pool.balls.find((b) => b.id === lid))
179
+ .filter((b) => !!b)
180
+ .map((b) => ({
181
+ id: b.id,
182
+ type: b.type,
183
+ summary: b.summary,
184
+ buoyancy: (0, buoyancy_1.computeBuoyancy)(b.B0, b.H, pool.alpha, pool.lambda, pool.t, b.t_last, b.permanent),
185
+ }));
186
+ res.json({ id: ballId, content, buoyancy: buoy, linked_balls: linkedBalls });
187
+ }).catch((err) => {
188
+ if (!res.headersSent)
189
+ res.status(500).json({ error: err.message || err });
190
+ });
191
+ });
192
+ // GET /api/memory-pool/global/sources
193
+ router.get('/global/sources', (_req, res) => {
194
+ const sources = (0, global_pool_manager_1.readSources)();
195
+ res.json(sources);
196
+ });
197
+ // DELETE /api/memory-pool/global/sources/:projectId
198
+ router.delete('/global/sources/:projectId', (req, res) => {
199
+ const removed = (0, global_pool_manager_1.removeSource)(req.params.projectId);
200
+ if (!removed) {
201
+ res.status(404).json({ error: 'Source not found' });
202
+ return;
203
+ }
204
+ res.json({ success: true });
205
+ });
206
+ // POST /api/memory-pool/global/sync
207
+ router.post('/global/sync', (_req, res) => {
208
+ (0, global_pool_manager_1.syncToGlobal)().then((result) => {
209
+ res.json(result);
210
+ }).catch((err) => {
211
+ if (err.message === 'SYNC_IN_PROGRESS') {
212
+ res.status(409).json({ error: 'Sync already in progress' });
213
+ return;
214
+ }
215
+ if (!res.headersSent)
216
+ res.status(500).json({ error: 'Sync failed: ' + (err.message || err) });
217
+ });
218
+ });
219
+ // ══════════════════════════════════════════════════════════════════════════════
220
+ // Project Memory Pool Endpoints
221
+ // ══════════════════════════════════════════════════════════════════════════════
59
222
  // GET /api/memory-pool/:projectId/status
60
223
  router.get('/:projectId/status', (req, res) => {
61
224
  const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
@@ -66,7 +229,6 @@ router.get('/:projectId/status', (req, res) => {
66
229
  res.json({ initialized: false });
67
230
  return;
68
231
  }
69
- // Try v2 format first
70
232
  const pool = (0, pool_manager_1.readPool)(poolDir);
71
233
  if (pool) {
72
234
  res.json({
@@ -78,6 +240,7 @@ router.get('/:projectId/status', (req, res) => {
78
240
  lambda: pool.lambda,
79
241
  alpha: pool.alpha,
80
242
  active_capacity: pool.active_capacity,
243
+ surface_width: pool.surface_width ?? 10000,
81
244
  next_id: pool.next_id,
82
245
  pool: pool.pool,
83
246
  initialized_at: pool.initialized_at,
@@ -86,7 +249,6 @@ router.get('/:projectId/status', (req, res) => {
86
249
  });
87
250
  return;
88
251
  }
89
- // Fall back to v1 format
90
252
  const stateFile = path.join(poolDir, 'state.json');
91
253
  try {
92
254
  const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
@@ -108,42 +270,51 @@ router.post('/:projectId/init', (req, res) => {
108
270
  if (!folder)
109
271
  return;
110
272
  const poolDir = path.join(folder, MEMORY_POOL_DIR);
111
- if ((0, pool_manager_1.isInitialized)(poolDir)) {
112
- res.status(409).json({ error: 'Memory pool already initialized' });
113
- return;
114
- }
115
- // Create directory structure
116
- fs.mkdirSync(path.join(poolDir, 'balls'), { recursive: true });
117
- // Generate documents
118
- (0, config_1.atomicWriteSync)(path.join(poolDir, 'SPEC.md'), (0, templates_1.generateSpecMd)());
119
- (0, config_1.atomicWriteSync)(path.join(poolDir, 'QUICK-REF.md'), (0, templates_1.generateQuickRefMd)());
120
- // Create pool.json (v2 format directly)
121
- const now = new Date().toISOString();
122
- const pool = {
123
- version: 2,
124
- t: 0,
125
- lambda: 0.97,
126
- alpha: 1.0,
127
- active_capacity: 20,
128
- next_id: 1,
129
- pool: 'project',
130
- initialized_at: now,
131
- balls: [],
132
- };
133
- (0, pool_manager_1.writePool)(poolDir, pool);
134
- // Append to CLAUDE.md if marker not present
135
- const claudeMdPath = path.join(folder, 'CLAUDE.md');
136
- try {
137
- const existing = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, 'utf-8') : '';
138
- if (!existing.includes(CLAUDE_MD_MARKER)) {
139
- const block = (0, templates_1.generateClaudeMdBlock)();
140
- (0, config_1.atomicWriteSync)(claudeMdPath, existing + '\n' + block);
273
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
274
+ if ((0, pool_manager_1.isInitialized)(poolDir)) {
275
+ res.status(409).json({ error: 'Memory pool already initialized' });
276
+ return;
141
277
  }
142
- }
143
- catch {
144
- // Non-fatal
145
- }
146
- res.json({ success: true });
278
+ fs.mkdirSync(path.join(poolDir, 'balls'), { recursive: true });
279
+ (0, config_1.atomicWriteSync)(path.join(poolDir, 'SPEC.md'), (0, templates_1.generateSpecMd)());
280
+ (0, config_1.atomicWriteSync)(path.join(poolDir, 'QUICK-REF.md'), (0, templates_1.generateQuickRefMd)());
281
+ const now = new Date().toISOString();
282
+ const pool = {
283
+ version: 2,
284
+ t: 0,
285
+ lambda: 0.97,
286
+ alpha: 1.0,
287
+ active_capacity: 20,
288
+ surface_width: 10000,
289
+ next_id: 1,
290
+ pool: 'project',
291
+ initialized_at: now,
292
+ balls: [],
293
+ };
294
+ (0, pool_manager_1.writePool)(poolDir, pool);
295
+ try {
296
+ const project = (0, config_1.getProject)(req.params.projectId);
297
+ if (project) {
298
+ (0, global_pool_manager_1.registerProject)(req.params.projectId, project.name, poolDir);
299
+ pool.global_pool_path = (0, global_pool_manager_1.getGlobalPoolDir)();
300
+ (0, pool_manager_1.writePool)(poolDir, pool);
301
+ }
302
+ }
303
+ catch { /* non-fatal */ }
304
+ const claudeMdPath = path.join(folder, 'CLAUDE.md');
305
+ try {
306
+ const existing = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, 'utf-8') : '';
307
+ if (!existing.includes(CLAUDE_MD_MARKER)) {
308
+ (0, config_1.atomicWriteSync)(claudeMdPath, existing + '\n' + (0, templates_1.generateClaudeMdBlock)());
309
+ }
310
+ }
311
+ catch { /* non-fatal */ }
312
+ (0, pool_manager_1.buildSurface)(poolDir);
313
+ res.json({ success: true });
314
+ }).catch((err) => {
315
+ if (!res.headersSent)
316
+ res.status(500).json({ error: err.message || err });
317
+ });
147
318
  });
148
319
  // POST /api/memory-pool/:projectId/upgrade
149
320
  router.post('/:projectId/upgrade', (req, res) => {
@@ -155,20 +326,17 @@ router.post('/:projectId/upgrade', (req, res) => {
155
326
  res.status(404).json({ error: 'Memory pool not initialized' });
156
327
  return;
157
328
  }
158
- try {
329
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
159
330
  const allChanges = [];
160
- // Step 1: Migrate data format (v1 → v2)
161
331
  if ((0, pool_manager_1.needsUpgrade)(poolDir)) {
162
332
  const { changes } = (0, pool_manager_1.migrateV1toV2)(poolDir);
163
333
  allChanges.push(...changes);
164
334
  }
165
- // Step 2: Regenerate documentation files (always update to latest templates)
166
335
  const pool = (0, pool_manager_1.readPool)(poolDir);
167
336
  if (pool) {
168
337
  (0, config_1.atomicWriteSync)(path.join(poolDir, 'SPEC.md'), (0, templates_1.generateSpecMd)());
169
338
  (0, config_1.atomicWriteSync)(path.join(poolDir, 'QUICK-REF.md'), (0, templates_1.generateQuickRefMd)());
170
339
  allChanges.push('updated SPEC.md and QUICK-REF.md');
171
- // Step 3: Update CLAUDE.md block
172
340
  const claudeMdPath = path.join(folder, 'CLAUDE.md');
173
341
  try {
174
342
  if (fs.existsSync(claudeMdPath)) {
@@ -188,18 +356,27 @@ router.post('/:projectId/upgrade', (req, res) => {
188
356
  allChanges.push('updated CLAUDE.md memory pool section');
189
357
  }
190
358
  }
191
- catch {
192
- // Non-fatal
359
+ catch { /* non-fatal */ }
360
+ try {
361
+ const project = (0, config_1.getProject)(req.params.projectId);
362
+ if (project) {
363
+ (0, global_pool_manager_1.registerProject)(req.params.projectId, project.name, poolDir);
364
+ pool.global_pool_path = (0, global_pool_manager_1.getGlobalPoolDir)();
365
+ (0, pool_manager_1.writePool)(poolDir, pool);
366
+ allChanges.push('registered with global memory pool');
367
+ }
193
368
  }
369
+ catch { /* non-fatal */ }
370
+ (0, pool_manager_1.buildSurface)(poolDir);
194
371
  res.json({ success: true, version: pool.version, changes: allChanges });
195
372
  }
196
373
  else {
197
374
  res.status(500).json({ error: 'Failed to read pool after migration' });
198
375
  }
199
- }
200
- catch (err) {
201
- res.status(500).json({ error: 'Upgrade failed: ' + (err.message || err) });
202
- }
376
+ }).catch((err) => {
377
+ if (!res.headersSent)
378
+ res.status(500).json({ error: 'Upgrade failed: ' + (err.message || err) });
379
+ });
203
380
  });
204
381
  // GET /api/memory-pool/:projectId/index
205
382
  router.get('/:projectId/index', (req, res) => {
@@ -207,14 +384,12 @@ router.get('/:projectId/index', (req, res) => {
207
384
  if (!folder)
208
385
  return;
209
386
  const poolDir = path.join(folder, MEMORY_POOL_DIR);
210
- // Try v2 format
211
387
  const pool = (0, pool_manager_1.readPool)(poolDir);
212
388
  if (pool) {
213
389
  const balls = (0, pool_manager_1.enrichBallsWithBuoyancy)(pool);
214
- res.json({ t: pool.t, updated_at: new Date().toISOString(), balls });
390
+ res.json({ t: pool.t, updated_at: new Date().toISOString(), balls, active_capacity: pool.active_capacity });
215
391
  return;
216
392
  }
217
- // Fall back to v1 format (read index.json directly)
218
393
  const indexFile = path.join(poolDir, 'index.json');
219
394
  try {
220
395
  const data = JSON.parse(fs.readFileSync(indexFile, 'utf-8'));
@@ -238,14 +413,9 @@ router.get('/:projectId/snapshot', (req, res) => {
238
413
  const cap = pool.active_capacity;
239
414
  const ballCount = pool.balls.length;
240
415
  const snapshot = (0, pool_manager_1.generateSnapshot)(pool);
241
- res.json({
242
- snapshot,
243
- t: pool.t,
244
- activeCount: Math.min(ballCount, cap),
245
- deepCount: Math.max(0, ballCount - cap),
246
- });
416
+ res.json({ snapshot, t: pool.t, activeCount: Math.min(ballCount, cap), deepCount: Math.max(0, ballCount - cap) });
247
417
  });
248
- // GET /api/memory-pool/:projectId/ball/:ballId
418
+ // GET /api/memory-pool/:projectId/ball/:ballId (pure read, no side effect)
249
419
  router.get('/:projectId/ball/:ballId', (req, res) => {
250
420
  const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
251
421
  if (!folder)
@@ -263,5 +433,395 @@ router.get('/:projectId/ball/:ballId', (req, res) => {
263
433
  }
264
434
  res.json({ id: ballId, content });
265
435
  });
436
+ // PUT /api/memory-pool/:projectId/surface-width
437
+ router.put('/:projectId/surface-width', (req, res) => {
438
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
439
+ if (!folder)
440
+ return;
441
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
442
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
443
+ const pool = (0, pool_manager_1.readPool)(poolDir);
444
+ if (!pool) {
445
+ res.status(404).json({ error: 'Memory pool not initialized' });
446
+ return;
447
+ }
448
+ const { surface_width } = req.body;
449
+ if (typeof surface_width !== 'number' || surface_width < 1000 || surface_width > 100000) {
450
+ res.status(400).json({ error: 'surface_width must be a number between 1000 and 100000' });
451
+ return;
452
+ }
453
+ pool.surface_width = surface_width;
454
+ (0, pool_manager_1.writePool)(poolDir, pool);
455
+ const { surfaceBalls, totalTokens } = (0, pool_manager_1.buildSurface)(poolDir);
456
+ res.json({ success: true, surface_width, surface_balls: surfaceBalls.length, total_tokens: totalTokens });
457
+ }).catch((err) => {
458
+ if (!res.headersSent)
459
+ res.status(500).json({ error: err.message || err });
460
+ });
461
+ });
462
+ // ══════════════════════════════════════════════════════════════════════════════
463
+ // Ball CRUD Endpoints (ccweb-managed)
464
+ // ══════════════════════════════════════════════════════════════════════════════
465
+ // POST /api/memory-pool/:projectId/balls — Create ball
466
+ router.post('/:projectId/balls', (req, res) => {
467
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
468
+ if (!folder)
469
+ return;
470
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
471
+ const { type, summary, content, links, b0_override } = req.body;
472
+ // Validate required fields
473
+ if (!type || !VALID_TYPES.includes(type)) {
474
+ res.status(400).json({ error: `type must be one of: ${VALID_TYPES.join(', ')}` });
475
+ return;
476
+ }
477
+ if (!summary || typeof summary !== 'string' || summary.length > 500) {
478
+ res.status(400).json({ error: 'summary is required and must be <= 500 characters' });
479
+ return;
480
+ }
481
+ if (!content || typeof content !== 'string' || content.length > 100000) {
482
+ res.status(400).json({ error: 'content is required and must be <= 100KB' });
483
+ return;
484
+ }
485
+ if (links && (!Array.isArray(links) || !validateBallIds(links))) {
486
+ res.status(400).json({ error: 'links must be an array of valid ball IDs' });
487
+ return;
488
+ }
489
+ if (b0_override !== undefined && b0_override !== null &&
490
+ (typeof b0_override !== 'number' || b0_override < 1 || b0_override > 10)) {
491
+ res.status(400).json({ error: 'b0_override must be a number between 1 and 10' });
492
+ return;
493
+ }
494
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
495
+ const pool = (0, pool_manager_1.readPool)(poolDir);
496
+ if (!pool) {
497
+ res.status(404).json({ error: 'Memory pool not initialized' });
498
+ return;
499
+ }
500
+ // M-2 fix: validate that linked ball IDs exist in pool
501
+ const validLinks = [];
502
+ if (links) {
503
+ const existingIds = new Set(pool.balls.map((b) => b.id));
504
+ for (const lid of links) {
505
+ if (!existingIds.has(lid)) {
506
+ res.status(400).json({ error: `Linked ball ${lid} does not exist` });
507
+ return;
508
+ }
509
+ validLinks.push(lid);
510
+ }
511
+ }
512
+ const ballId = `ball_${String(pool.next_id).padStart(4, '0')}`;
513
+ pool.next_id++;
514
+ const B0 = b0_override ?? DEFAULT_B0[type] ?? 5;
515
+ const diameter = (0, pool_manager_1.estimateTokens)(content);
516
+ // Write ball content first (gate pattern)
517
+ (0, pool_manager_1.writeBallContent)(poolDir, ballId, content);
518
+ const newBall = {
519
+ id: ballId,
520
+ type: type,
521
+ summary,
522
+ B0,
523
+ H: 0,
524
+ t_last: pool.t,
525
+ hardness: 5,
526
+ permanent: false,
527
+ links: validLinks,
528
+ created_at: new Date().toISOString(),
529
+ diameter,
530
+ };
531
+ pool.balls.push(newBall);
532
+ (0, pool_manager_1.writePool)(poolDir, pool);
533
+ (0, pool_manager_1.buildSurface)(poolDir);
534
+ res.json({ id: ballId, B0, diameter });
535
+ }).catch((err) => {
536
+ if (!res.headersSent)
537
+ res.status(500).json({ error: err.message || err });
538
+ });
539
+ });
540
+ // PUT /api/memory-pool/:projectId/balls/:ballId — Update ball metadata after content edit
541
+ router.put('/:projectId/balls/:ballId', (req, res) => {
542
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
543
+ if (!folder)
544
+ return;
545
+ const { ballId } = req.params;
546
+ if (!BALL_ID_RE.test(ballId)) {
547
+ res.status(400).json({ error: 'Invalid ball ID format' });
548
+ return;
549
+ }
550
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
551
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
552
+ const pool = (0, pool_manager_1.readPool)(poolDir);
553
+ if (!pool) {
554
+ res.status(404).json({ error: 'Memory pool not initialized' });
555
+ return;
556
+ }
557
+ const ball = pool.balls.find((b) => b.id === ballId);
558
+ if (!ball) {
559
+ res.status(404).json({ error: 'Ball not found in pool' });
560
+ return;
561
+ }
562
+ // Recalculate diameter from current content
563
+ const diameter = (0, pool_manager_1.computeDiameter)(poolDir, ballId);
564
+ ball.diameter = diameter;
565
+ // Update summary if provided
566
+ const { summary } = req.body;
567
+ if (summary && typeof summary === 'string') {
568
+ ball.summary = summary;
569
+ }
570
+ (0, pool_manager_1.writePool)(poolDir, pool);
571
+ (0, pool_manager_1.buildSurface)(poolDir);
572
+ res.json({ id: ballId, diameter, summary: ball.summary });
573
+ }).catch((err) => {
574
+ if (!res.headersSent)
575
+ res.status(500).json({ error: err.message || err });
576
+ });
577
+ });
578
+ // POST /api/memory-pool/:projectId/balls/:ballId/hit — Hit query (read + count)
579
+ router.post('/:projectId/balls/:ballId/hit', (req, res) => {
580
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
581
+ if (!folder)
582
+ return;
583
+ const { ballId } = req.params;
584
+ if (!BALL_ID_RE.test(ballId)) {
585
+ res.status(400).json({ error: 'Invalid ball ID format' });
586
+ return;
587
+ }
588
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
589
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
590
+ const pool = (0, pool_manager_1.readPool)(poolDir);
591
+ if (!pool) {
592
+ res.status(404).json({ error: 'Memory pool not initialized' });
593
+ return;
594
+ }
595
+ const ball = pool.balls.find((b) => b.id === ballId);
596
+ if (!ball) {
597
+ res.status(404).json({ error: 'Ball not found' });
598
+ return;
599
+ }
600
+ // Update hit count
601
+ ball.H += 1;
602
+ ball.t_last = pool.t;
603
+ (0, pool_manager_1.writePool)(poolDir, pool);
604
+ // Skip surface rebuild for hit — H change rarely affects surface order
605
+ // Read content
606
+ const content = (0, pool_manager_1.readBallContent)(poolDir, ballId);
607
+ const buoy = (0, buoyancy_1.computeBuoyancy)(ball.B0, ball.H, pool.alpha, pool.lambda, pool.t, ball.t_last, ball.permanent);
608
+ // Read linked balls' summaries
609
+ const linkedBalls = ball.links
610
+ .map((lid) => pool.balls.find((b) => b.id === lid))
611
+ .filter((b) => !!b)
612
+ .map((b) => ({
613
+ id: b.id,
614
+ type: b.type,
615
+ summary: b.summary,
616
+ buoyancy: (0, buoyancy_1.computeBuoyancy)(b.B0, b.H, pool.alpha, pool.lambda, pool.t, b.t_last, b.permanent),
617
+ }));
618
+ res.json({ id: ballId, content, buoyancy: buoy, linked_balls: linkedBalls });
619
+ }).catch((err) => {
620
+ if (!res.headersSent)
621
+ res.status(500).json({ error: err.message || err });
622
+ });
623
+ });
624
+ // DELETE /api/memory-pool/:projectId/balls/:ballId — Delete ball
625
+ router.delete('/:projectId/balls/:ballId', (req, res) => {
626
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
627
+ if (!folder)
628
+ return;
629
+ const { ballId } = req.params;
630
+ if (!BALL_ID_RE.test(ballId)) {
631
+ res.status(400).json({ error: 'Invalid ball ID format' });
632
+ return;
633
+ }
634
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
635
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
636
+ const pool = (0, pool_manager_1.readPool)(poolDir);
637
+ if (!pool) {
638
+ res.status(404).json({ error: 'Memory pool not initialized' });
639
+ return;
640
+ }
641
+ const idx = pool.balls.findIndex((b) => b.id === ballId);
642
+ if (idx === -1) {
643
+ res.status(404).json({ error: 'Ball not found' });
644
+ return;
645
+ }
646
+ // Remove from balls array
647
+ pool.balls.splice(idx, 1);
648
+ // Clean up links in other balls that reference this ball
649
+ let linksCleaned = 0;
650
+ for (const b of pool.balls) {
651
+ const before = b.links.length;
652
+ b.links = b.links.filter((l) => l !== ballId);
653
+ linksCleaned += before - b.links.length;
654
+ }
655
+ (0, pool_manager_1.writePool)(poolDir, pool);
656
+ // Delete ball file
657
+ const ballFile = path.join(poolDir, 'balls', `${ballId}.md`);
658
+ try {
659
+ fs.unlinkSync(ballFile);
660
+ }
661
+ catch { /* may not exist */ }
662
+ (0, pool_manager_1.buildSurface)(poolDir);
663
+ res.json({ id: ballId, deleted: true, links_cleaned: linksCleaned });
664
+ }).catch((err) => {
665
+ if (!res.headersSent)
666
+ res.status(500).json({ error: err.message || err });
667
+ });
668
+ });
669
+ // PATCH /api/memory-pool/:projectId/balls/:ballId/links — Manage links
670
+ router.patch('/:projectId/balls/:ballId/links', (req, res) => {
671
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
672
+ if (!folder)
673
+ return;
674
+ const { ballId } = req.params;
675
+ if (!BALL_ID_RE.test(ballId)) {
676
+ res.status(400).json({ error: 'Invalid ball ID format' });
677
+ return;
678
+ }
679
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
680
+ const { add, remove } = req.body;
681
+ if (add && (!Array.isArray(add) || !validateBallIds(add))) {
682
+ res.status(400).json({ error: 'add must be an array of valid ball IDs' });
683
+ return;
684
+ }
685
+ if (remove && (!Array.isArray(remove) || !validateBallIds(remove))) {
686
+ res.status(400).json({ error: 'remove must be an array of valid ball IDs' });
687
+ return;
688
+ }
689
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
690
+ const pool = (0, pool_manager_1.readPool)(poolDir);
691
+ if (!pool) {
692
+ res.status(404).json({ error: 'Memory pool not initialized' });
693
+ return;
694
+ }
695
+ const ball = pool.balls.find((b) => b.id === ballId);
696
+ if (!ball) {
697
+ res.status(404).json({ error: 'Ball not found' });
698
+ return;
699
+ }
700
+ const existingIds = new Set(pool.balls.map((b) => b.id));
701
+ if (remove) {
702
+ const removeSet = new Set(remove);
703
+ ball.links = ball.links.filter((l) => !removeSet.has(l));
704
+ }
705
+ if (add) {
706
+ for (const lid of add) {
707
+ if (existingIds.has(lid) && !ball.links.includes(lid) && lid !== ballId) {
708
+ ball.links.push(lid);
709
+ }
710
+ }
711
+ }
712
+ (0, pool_manager_1.writePool)(poolDir, pool);
713
+ (0, pool_manager_1.buildSurface)(poolDir);
714
+ res.json({ id: ballId, links: ball.links });
715
+ }).catch((err) => {
716
+ if (!res.headersSent)
717
+ res.status(500).json({ error: err.message || err });
718
+ });
719
+ });
720
+ // POST /api/memory-pool/:projectId/tick — Increment turn
721
+ router.post('/:projectId/tick', (req, res) => {
722
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
723
+ if (!folder)
724
+ return;
725
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
726
+ const { session } = req.body || {};
727
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
728
+ const result = (0, pool_manager_1.tickPool)(poolDir, session);
729
+ if (!result) {
730
+ res.status(404).json({ error: 'Memory pool not initialized' });
731
+ return;
732
+ }
733
+ res.json(result);
734
+ }).catch((err) => {
735
+ if (!res.headersSent)
736
+ res.status(500).json({ error: err.message || err });
737
+ });
738
+ });
739
+ // GET /api/memory-pool/:projectId/surface — Get active layer summary
740
+ router.get('/:projectId/surface', (req, res) => {
741
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
742
+ if (!folder)
743
+ return;
744
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
745
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
746
+ const pool = (0, pool_manager_1.readPool)(poolDir);
747
+ if (!pool) {
748
+ res.status(404).json({ error: 'Memory pool not initialized' });
749
+ return;
750
+ }
751
+ // Compensate missed tick if last_tick_at > 10 minutes ago
752
+ if (pool.last_tick_at) {
753
+ const elapsed = Date.now() - new Date(pool.last_tick_at).getTime();
754
+ if (elapsed > 10 * 60 * 1000) {
755
+ pool.t += 1;
756
+ pool.last_tick_at = new Date().toISOString();
757
+ pool.last_tick_session = undefined;
758
+ (0, pool_manager_1.writePool)(poolDir, pool);
759
+ }
760
+ }
761
+ const { surfaceBalls, totalTokens } = (0, pool_manager_1.buildSurface)(poolDir);
762
+ res.json({
763
+ t: pool.t,
764
+ surface_width: pool.surface_width ?? 10000,
765
+ used_tokens: totalTokens,
766
+ balls: surfaceBalls,
767
+ });
768
+ }).catch((err) => {
769
+ if (!res.headersSent)
770
+ res.status(500).json({ error: err.message || err });
771
+ });
772
+ });
773
+ // POST /api/memory-pool/:projectId/maintenance — Maintenance suggestions + self-healing
774
+ router.post('/:projectId/maintenance', (req, res) => {
775
+ const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
776
+ if (!folder)
777
+ return;
778
+ const poolDir = path.join(folder, MEMORY_POOL_DIR);
779
+ const pool = (0, pool_manager_1.readPool)(poolDir);
780
+ if (!pool) {
781
+ res.status(404).json({ error: 'Memory pool not initialized' });
782
+ return;
783
+ }
784
+ const enriched = (0, pool_manager_1.enrichBallsWithBuoyancy)(pool);
785
+ const cap = pool.active_capacity;
786
+ const activeFull = enriched.length >= cap;
787
+ // Split suggestions: all active layer balls, mark recommended based on hardness + size
788
+ const suggestions = [];
789
+ const activeBalls = enriched.slice(0, cap);
790
+ for (const b of activeBalls) {
791
+ const diameter = b.diameter ?? (0, pool_manager_1.computeDiameter)(poolDir, b.id);
792
+ if (diameter > 100) {
793
+ const recommended = b.hardness < 7;
794
+ suggestions.push({
795
+ action: 'split',
796
+ ball_id: b.id,
797
+ reason: `diameter=${diameter}tok, hardness=${b.hardness}${recommended ? ' — 可考虑拆分' : ' — 硬度过高,不建议拆分'}`,
798
+ recommended,
799
+ });
800
+ }
801
+ }
802
+ // Self-healing: detect inconsistencies (H-4 fix)
803
+ const anomalies = [];
804
+ const ballsDir = path.join(poolDir, 'balls');
805
+ // Check for ghost entries (pool.json has ball but file missing)
806
+ for (const b of pool.balls) {
807
+ const file = path.join(ballsDir, `${b.id}.md`);
808
+ if (!fs.existsSync(file)) {
809
+ anomalies.push(`ghost: ${b.id} in pool.json but file missing`);
810
+ }
811
+ }
812
+ // Check for orphan files (file exists but not in pool.json)
813
+ try {
814
+ const poolIds = new Set(pool.balls.map((b) => b.id));
815
+ const files = fs.readdirSync(ballsDir).filter((f) => f.endsWith('.md') && f.startsWith('ball_'));
816
+ for (const f of files) {
817
+ const id = f.replace('.md', '');
818
+ if (!poolIds.has(id)) {
819
+ anomalies.push(`orphan_file: ${f} exists but not in pool.json`);
820
+ }
821
+ }
822
+ }
823
+ catch { /* balls dir may not exist */ }
824
+ res.json({ active_full: activeFull, suggestions, anomalies });
825
+ });
266
826
  exports.default = router;
267
827
  //# sourceMappingURL=memory-pool.js.map