@tom2012/cc-web 1.5.93 → 1.5.95

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 (45) hide show
  1. package/README.md +1 -1
  2. package/backend/dist/memory-pool/global-pool-manager.d.ts +2 -11
  3. package/backend/dist/memory-pool/global-pool-manager.d.ts.map +1 -1
  4. package/backend/dist/memory-pool/global-pool-manager.js +126 -232
  5. package/backend/dist/memory-pool/global-pool-manager.js.map +1 -1
  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 +97 -131
  16. package/backend/dist/memory-pool/templates.js.map +1 -1
  17. package/backend/dist/memory-pool/types.d.ts +4 -8
  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 +505 -95
  24. package/backend/dist/routes/memory-pool.js.map +1 -1
  25. package/frontend/dist/assets/{GraphPreview-CSZTNhQX.js → GraphPreview-BFYA_WA5.js} +1 -1
  26. package/frontend/dist/assets/{OfficePreview-DZmKFnER.js → OfficePreview-DN-it4Af.js} +2 -2
  27. package/frontend/dist/assets/{PlanPanel-DuAa0AZZ.js → PlanPanel-DYupjf3X.js} +1 -1
  28. package/frontend/dist/assets/{ProjectPage-BxuE0x69.js → ProjectPage-B6tVjIYr.js} +5 -5
  29. package/frontend/dist/assets/{SettingsPage-DSnQze4Z.js → SettingsPage-CQL_FfkR.js} +1 -1
  30. package/frontend/dist/assets/{ShareViewPage-Dmur70tT.js → ShareViewPage-C_x31zAw.js} +1 -1
  31. package/frontend/dist/assets/{SkillHubPage-DnGb0OHx.js → SkillHubPage-Cgoxl30L.js} +2 -2
  32. package/frontend/dist/assets/{bot-DKy4a5yK.js → bot-Dst2XqWX.js} +1 -1
  33. package/frontend/dist/assets/{chevron-down-B-GU2wv8.js → chevron-down-DsAldQb3.js} +1 -1
  34. package/frontend/dist/assets/{download-CsOUNuPy.js → download-YA6lXM4C.js} +1 -1
  35. package/frontend/dist/assets/index-BDYnq-Fr.css +1 -0
  36. package/frontend/dist/assets/{index-C202jcvT.js → index-CY9Au1bg.js} +4 -4
  37. package/frontend/dist/assets/{index-I4_xf4g_.js → index-_m8u9esJ.js} +1 -1
  38. package/frontend/dist/assets/{jszip.min-Dc0FmeMP.js → jszip.min-BC9RCrTS.js} +1 -1
  39. package/frontend/dist/assets/{matter-Da3rDMpe.js → matter-Mm9BJmx6.js} +1 -1
  40. package/frontend/dist/assets/{maximize-2-BEyzEiyH.js → maximize-2-CuzznumM.js} +1 -1
  41. package/frontend/dist/assets/{save-BYSx9fYK.js → save-Q2W6S7fk.js} +1 -1
  42. package/frontend/dist/assets/{user-D3KuBJwv.js → user-BM9GY1Xb.js} +1 -1
  43. package/frontend/dist/index.html +2 -2
  44. package/package.json +1 -1
  45. package/frontend/dist/assets/index-BiVVvGoh.css +0 -1
@@ -39,9 +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");
42
44
  const global_pool_manager_1 = require("../memory-pool/global-pool-manager");
43
45
  const router = (0, express_1.Router)();
44
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 };
45
49
  const MEMORY_POOL_DIR = '.memory-pool';
46
50
  const CLAUDE_MD_MARKER = '## 记忆池(Memory Pool)';
47
51
  function resolveProjectFolder(projectId, username, res) {
@@ -57,6 +61,9 @@ function resolveProjectFolder(projectId, username, res) {
57
61
  }
58
62
  return project.folderPath;
59
63
  }
64
+ function validateBallIds(ids) {
65
+ return ids.every((id) => typeof id === 'string' && BALL_ID_RE.test(id));
66
+ }
60
67
  // ══════════════════════════════════════════════════════════════════════════════
61
68
  // Global Memory Pool Endpoints (MUST be before /:projectId routes)
62
69
  // ══════════════════════════════════════════════════════════════════════════════
@@ -81,6 +88,7 @@ router.get('/global/status', (_req, res) => {
81
88
  lambda: pool.lambda,
82
89
  alpha: pool.alpha,
83
90
  active_capacity: pool.active_capacity,
91
+ surface_width: pool.surface_width ?? 10000,
84
92
  next_id: pool.next_id,
85
93
  pool: pool.pool,
86
94
  initialized_at: pool.initialized_at,
@@ -97,12 +105,37 @@ router.get('/global/index', (_req, res) => {
97
105
  res.status(404).json({ error: 'Global pool not initialized' });
98
106
  return;
99
107
  }
100
- // Use fresh calendar-based t for accurate buoyancy calculation
101
108
  pool.t = (0, global_pool_manager_1.computeGlobalT)(pool.initialized_at);
102
109
  const balls = (0, pool_manager_1.enrichBallsWithBuoyancy)(pool);
103
110
  res.json({ t: pool.t, updated_at: new Date().toISOString(), balls, active_capacity: pool.active_capacity });
104
111
  });
105
- // GET /api/memory-pool/global/ball/:ballId
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)
106
139
  router.get('/global/ball/:ballId', (req, res) => {
107
140
  const { ballId } = req.params;
108
141
  if (!BALL_ID_RE.test(ballId)) {
@@ -116,6 +149,46 @@ router.get('/global/ball/:ballId', (req, res) => {
116
149
  }
117
150
  res.json({ id: ballId, content });
118
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
+ });
119
192
  // GET /api/memory-pool/global/sources
120
193
  router.get('/global/sources', (_req, res) => {
121
194
  const sources = (0, global_pool_manager_1.readSources)();
@@ -132,17 +205,16 @@ router.delete('/global/sources/:projectId', (req, res) => {
132
205
  });
133
206
  // POST /api/memory-pool/global/sync
134
207
  router.post('/global/sync', (_req, res) => {
135
- try {
136
- const result = (0, global_pool_manager_1.syncToGlobal)();
208
+ (0, global_pool_manager_1.syncToGlobal)().then((result) => {
137
209
  res.json(result);
138
- }
139
- catch (err) {
210
+ }).catch((err) => {
140
211
  if (err.message === 'SYNC_IN_PROGRESS') {
141
212
  res.status(409).json({ error: 'Sync already in progress' });
142
213
  return;
143
214
  }
144
- res.status(500).json({ error: 'Sync failed: ' + (err.message || err) });
145
- }
215
+ if (!res.headersSent)
216
+ res.status(500).json({ error: 'Sync failed: ' + (err.message || err) });
217
+ });
146
218
  });
147
219
  // ══════════════════════════════════════════════════════════════════════════════
148
220
  // Project Memory Pool Endpoints
@@ -157,7 +229,6 @@ router.get('/:projectId/status', (req, res) => {
157
229
  res.json({ initialized: false });
158
230
  return;
159
231
  }
160
- // Try v2 format first
161
232
  const pool = (0, pool_manager_1.readPool)(poolDir);
162
233
  if (pool) {
163
234
  res.json({
@@ -169,6 +240,7 @@ router.get('/:projectId/status', (req, res) => {
169
240
  lambda: pool.lambda,
170
241
  alpha: pool.alpha,
171
242
  active_capacity: pool.active_capacity,
243
+ surface_width: pool.surface_width ?? 10000,
172
244
  next_id: pool.next_id,
173
245
  pool: pool.pool,
174
246
  initialized_at: pool.initialized_at,
@@ -177,7 +249,6 @@ router.get('/:projectId/status', (req, res) => {
177
249
  });
178
250
  return;
179
251
  }
180
- // Fall back to v1 format
181
252
  const stateFile = path.join(poolDir, 'state.json');
182
253
  try {
183
254
  const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
@@ -199,52 +270,51 @@ router.post('/:projectId/init', (req, res) => {
199
270
  if (!folder)
200
271
  return;
201
272
  const poolDir = path.join(folder, MEMORY_POOL_DIR);
202
- if ((0, pool_manager_1.isInitialized)(poolDir)) {
203
- res.status(409).json({ error: 'Memory pool already initialized' });
204
- return;
205
- }
206
- // Create directory structure
207
- fs.mkdirSync(path.join(poolDir, 'balls'), { recursive: true });
208
- // Generate documents
209
- (0, config_1.atomicWriteSync)(path.join(poolDir, 'SPEC.md'), (0, templates_1.generateSpecMd)());
210
- (0, config_1.atomicWriteSync)(path.join(poolDir, 'QUICK-REF.md'), (0, templates_1.generateQuickRefMd)());
211
- // Create pool.json (v2 format directly)
212
- const now = new Date().toISOString();
213
- const pool = {
214
- version: 2,
215
- t: 0,
216
- lambda: 0.97,
217
- alpha: 1.0,
218
- active_capacity: 20,
219
- next_id: 1,
220
- pool: 'project',
221
- initialized_at: now,
222
- balls: [],
223
- };
224
- (0, pool_manager_1.writePool)(poolDir, pool);
225
- // Register with global pool
226
- try {
227
- const project = (0, config_1.getProject)(req.params.projectId);
228
- if (project) {
229
- (0, global_pool_manager_1.registerProject)(req.params.projectId, project.name, poolDir);
230
- pool.global_pool_path = (0, global_pool_manager_1.getGlobalPoolDir)();
231
- (0, pool_manager_1.writePool)(poolDir, pool);
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;
232
277
  }
233
- }
234
- catch { /* non-fatal */ }
235
- // Append to CLAUDE.md if marker not present
236
- const claudeMdPath = path.join(folder, 'CLAUDE.md');
237
- try {
238
- const existing = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, 'utf-8') : '';
239
- if (!existing.includes(CLAUDE_MD_MARKER)) {
240
- const block = (0, templates_1.generateClaudeMdBlock)();
241
- (0, config_1.atomicWriteSync)(claudeMdPath, existing + '\n' + block);
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
+ }
242
302
  }
243
- }
244
- catch {
245
- // Non-fatal
246
- }
247
- res.json({ success: true });
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
+ });
248
318
  });
249
319
  // POST /api/memory-pool/:projectId/upgrade
250
320
  router.post('/:projectId/upgrade', (req, res) => {
@@ -256,20 +326,17 @@ router.post('/:projectId/upgrade', (req, res) => {
256
326
  res.status(404).json({ error: 'Memory pool not initialized' });
257
327
  return;
258
328
  }
259
- try {
329
+ (0, pool_lock_1.withPoolLock)(poolDir, () => {
260
330
  const allChanges = [];
261
- // Step 1: Migrate data format (v1 → v2)
262
331
  if ((0, pool_manager_1.needsUpgrade)(poolDir)) {
263
332
  const { changes } = (0, pool_manager_1.migrateV1toV2)(poolDir);
264
333
  allChanges.push(...changes);
265
334
  }
266
- // Step 2: Regenerate documentation files (always update to latest templates)
267
335
  const pool = (0, pool_manager_1.readPool)(poolDir);
268
336
  if (pool) {
269
337
  (0, config_1.atomicWriteSync)(path.join(poolDir, 'SPEC.md'), (0, templates_1.generateSpecMd)());
270
338
  (0, config_1.atomicWriteSync)(path.join(poolDir, 'QUICK-REF.md'), (0, templates_1.generateQuickRefMd)());
271
339
  allChanges.push('updated SPEC.md and QUICK-REF.md');
272
- // Step 3: Update CLAUDE.md block
273
340
  const claudeMdPath = path.join(folder, 'CLAUDE.md');
274
341
  try {
275
342
  if (fs.existsSync(claudeMdPath)) {
@@ -289,10 +356,7 @@ router.post('/:projectId/upgrade', (req, res) => {
289
356
  allChanges.push('updated CLAUDE.md memory pool section');
290
357
  }
291
358
  }
292
- catch {
293
- // Non-fatal
294
- }
295
- // Step 4: Register with global pool
359
+ catch { /* non-fatal */ }
296
360
  try {
297
361
  const project = (0, config_1.getProject)(req.params.projectId);
298
362
  if (project) {
@@ -303,15 +367,16 @@ router.post('/:projectId/upgrade', (req, res) => {
303
367
  }
304
368
  }
305
369
  catch { /* non-fatal */ }
370
+ (0, pool_manager_1.buildSurface)(poolDir);
306
371
  res.json({ success: true, version: pool.version, changes: allChanges });
307
372
  }
308
373
  else {
309
374
  res.status(500).json({ error: 'Failed to read pool after migration' });
310
375
  }
311
- }
312
- catch (err) {
313
- res.status(500).json({ error: 'Upgrade failed: ' + (err.message || err) });
314
- }
376
+ }).catch((err) => {
377
+ if (!res.headersSent)
378
+ res.status(500).json({ error: 'Upgrade failed: ' + (err.message || err) });
379
+ });
315
380
  });
316
381
  // GET /api/memory-pool/:projectId/index
317
382
  router.get('/:projectId/index', (req, res) => {
@@ -319,14 +384,12 @@ router.get('/:projectId/index', (req, res) => {
319
384
  if (!folder)
320
385
  return;
321
386
  const poolDir = path.join(folder, MEMORY_POOL_DIR);
322
- // Try v2 format
323
387
  const pool = (0, pool_manager_1.readPool)(poolDir);
324
388
  if (pool) {
325
389
  const balls = (0, pool_manager_1.enrichBallsWithBuoyancy)(pool);
326
- 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 });
327
391
  return;
328
392
  }
329
- // Fall back to v1 format (read index.json directly)
330
393
  const indexFile = path.join(poolDir, 'index.json');
331
394
  try {
332
395
  const data = JSON.parse(fs.readFileSync(indexFile, 'utf-8'));
@@ -350,14 +413,9 @@ router.get('/:projectId/snapshot', (req, res) => {
350
413
  const cap = pool.active_capacity;
351
414
  const ballCount = pool.balls.length;
352
415
  const snapshot = (0, pool_manager_1.generateSnapshot)(pool);
353
- res.json({
354
- snapshot,
355
- t: pool.t,
356
- activeCount: Math.min(ballCount, cap),
357
- deepCount: Math.max(0, ballCount - cap),
358
- });
416
+ res.json({ snapshot, t: pool.t, activeCount: Math.min(ballCount, cap), deepCount: Math.max(0, ballCount - cap) });
359
417
  });
360
- // GET /api/memory-pool/:projectId/ball/:ballId
418
+ // GET /api/memory-pool/:projectId/ball/:ballId (pure read, no side effect)
361
419
  router.get('/:projectId/ball/:ballId', (req, res) => {
362
420
  const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
363
421
  if (!folder)
@@ -375,43 +433,395 @@ router.get('/:projectId/ball/:ballId', (req, res) => {
375
433
  }
376
434
  res.json({ id: ballId, content });
377
435
  });
378
- // GET /api/memory-pool/:projectId/import-preview
379
- router.get('/:projectId/import-preview', (req, res) => {
436
+ // PUT /api/memory-pool/:projectId/surface-width
437
+ router.put('/:projectId/surface-width', (req, res) => {
380
438
  const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
381
439
  if (!folder)
382
440
  return;
383
441
  const poolDir = path.join(folder, MEMORY_POOL_DIR);
384
- try {
385
- const preview = (0, global_pool_manager_1.getImportPreview)(poolDir);
386
- res.json({ balls: preview });
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;
387
480
  }
388
- catch (err) {
389
- res.status(500).json({ error: 'Preview failed: ' + (err.message || err) });
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;
390
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
+ });
391
539
  });
392
- // POST /api/memory-pool/:projectId/import-from-global
393
- router.post('/:projectId/import-from-global', (req, res) => {
540
+ // PUT /api/memory-pool/:projectId/balls/:ballId — Update ball metadata after content edit
541
+ router.put('/:projectId/balls/:ballId', (req, res) => {
394
542
  const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
395
543
  if (!folder)
396
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
+ }
397
550
  const poolDir = path.join(folder, MEMORY_POOL_DIR);
398
- const { ball_ids } = req.body;
399
- if (!Array.isArray(ball_ids) || ball_ids.length === 0) {
400
- res.status(400).json({ error: 'ball_ids array required' });
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' });
401
586
  return;
402
587
  }
403
- // C-1 fix: validate each ball_id to prevent path traversal
404
- if (!ball_ids.every((id) => typeof id === 'string' && BALL_ID_RE.test(id))) {
405
- res.status(400).json({ error: 'Invalid ball ID format in ball_ids' });
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' });
406
632
  return;
407
633
  }
408
- try {
409
- const result = (0, global_pool_manager_1.importFromGlobal)(poolDir, ball_ids);
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
+ }
410
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
+ }
411
801
  }
412
- catch (err) {
413
- res.status(500).json({ error: 'Import failed: ' + (err.message || err) });
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
+ }
414
822
  }
823
+ catch { /* balls dir may not exist */ }
824
+ res.json({ active_full: activeFull, suggestions, anomalies });
415
825
  });
416
826
  exports.default = router;
417
827
  //# sourceMappingURL=memory-pool.js.map