@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.
- package/README.md +1 -1
- package/backend/dist/memory-pool/global-pool-manager.d.ts +12 -0
- package/backend/dist/memory-pool/global-pool-manager.d.ts.map +1 -0
- package/backend/dist/memory-pool/global-pool-manager.js +290 -0
- package/backend/dist/memory-pool/global-pool-manager.js.map +1 -0
- package/backend/dist/memory-pool/pool-lock.d.ts +2 -0
- package/backend/dist/memory-pool/pool-lock.d.ts.map +1 -0
- package/backend/dist/memory-pool/pool-lock.js +31 -0
- package/backend/dist/memory-pool/pool-lock.js.map +1 -0
- package/backend/dist/memory-pool/pool-manager.d.ts +34 -0
- package/backend/dist/memory-pool/pool-manager.d.ts.map +1 -1
- package/backend/dist/memory-pool/pool-manager.js +91 -0
- package/backend/dist/memory-pool/pool-manager.js.map +1 -1
- package/backend/dist/memory-pool/templates.d.ts.map +1 -1
- package/backend/dist/memory-pool/templates.js +98 -127
- package/backend/dist/memory-pool/templates.js.map +1 -1
- package/backend/dist/memory-pool/types.d.ts +34 -0
- package/backend/dist/memory-pool/types.d.ts.map +1 -1
- package/backend/dist/routes/hooks.d.ts.map +1 -1
- package/backend/dist/routes/hooks.js +14 -0
- package/backend/dist/routes/hooks.js.map +1 -1
- package/backend/dist/routes/memory-pool.d.ts.map +1 -1
- package/backend/dist/routes/memory-pool.js +617 -57
- package/backend/dist/routes/memory-pool.js.map +1 -1
- package/frontend/dist/assets/{GraphPreview-QiSxy4LP.js → GraphPreview-BFYA_WA5.js} +1 -1
- package/frontend/dist/assets/{OfficePreview-DlUCjmFD.js → OfficePreview-DN-it4Af.js} +2 -2
- package/frontend/dist/assets/{PlanPanel-Dn5PCqCc.js → PlanPanel-DYupjf3X.js} +1 -1
- package/frontend/dist/assets/{ProjectPage-XG9MZm7t.js → ProjectPage-B6tVjIYr.js} +5 -5
- package/frontend/dist/assets/{SettingsPage-ofs1M1K_.js → SettingsPage-CQL_FfkR.js} +2 -2
- package/frontend/dist/assets/{ShareViewPage-u8FXEdhC.js → ShareViewPage-C_x31zAw.js} +1 -1
- package/frontend/dist/assets/{SkillHubPage-DYQ3--oW.js → SkillHubPage-Cgoxl30L.js} +2 -2
- package/frontend/dist/assets/{bot-63iGenXw.js → bot-Dst2XqWX.js} +1 -1
- package/frontend/dist/assets/{chevron-down-CzkwkeMW.js → chevron-down-DsAldQb3.js} +1 -1
- package/frontend/dist/assets/{download-DhFx0Fu-.js → download-YA6lXM4C.js} +1 -1
- package/frontend/dist/assets/{index-DVUjdhq8.css → index-BDYnq-Fr.css} +1 -1
- package/frontend/dist/assets/{index-C8aR4tRp.js → index-CY9Au1bg.js} +9 -9
- package/frontend/dist/assets/{index-DKsG2yPY.js → index-_m8u9esJ.js} +1 -1
- package/frontend/dist/assets/{jszip.min-B9vwwWDL.js → jszip.min-BC9RCrTS.js} +1 -1
- package/frontend/dist/assets/{matter-CCkX66fz.js → matter-Mm9BJmx6.js} +1 -1
- package/frontend/dist/assets/{maximize-2-B3mAuuFs.js → maximize-2-CuzznumM.js} +1 -1
- package/frontend/dist/assets/{save-CxQAMJ-_.js → save-Q2W6S7fk.js} +1 -1
- package/frontend/dist/assets/{user-DxwkzcYb.js → user-BM9GY1Xb.js} +1 -1
- package/frontend/dist/index.html +2 -2
- 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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|