pi2web 0.2.1 → 0.2.6
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 +44 -5
- package/dist/activity.js +178 -0
- package/dist/agent-feed.js +133 -0
- package/dist/agent-templates.js +144 -0
- package/dist/agents.js +518 -141
- package/dist/auth.js +3 -1
- package/dist/collab/baseline.js +84 -0
- package/dist/collab/dispatcher.js +45 -10
- package/dist/collab/hub.js +1392 -384
- package/dist/collab/pi-extension.js +322 -65
- package/dist/collab/review-flow.js +197 -48
- package/dist/collab/routes.js +94 -23
- package/dist/collab/schemas.js +69 -13
- package/dist/collab/scoring-flow.js +145 -39
- package/dist/collab/similarity.js +15 -0
- package/dist/collab/store.js +467 -112
- package/dist/collab/types.js +94 -10
- package/dist/collab/validate.js +62 -25
- package/dist/drafts.js +26 -6
- package/dist/http/agent-routes.js +196 -0
- package/dist/http/login-limiter.js +53 -0
- package/dist/http/mail-settings-routes.js +53 -0
- package/dist/http/system-prompt-routes.js +52 -0
- package/dist/http/utils.js +43 -0
- package/dist/http/web-assets.js +63 -0
- package/dist/http/workspace-routes.js +58 -0
- package/dist/mail-notifier.js +63 -17
- package/dist/main.js +61 -16
- package/dist/mcp.js +24 -5
- package/dist/metadata-store.js +190 -56
- package/dist/paths.js +10 -0
- package/dist/sdk-backend.js +221 -45
- package/dist/server.js +444 -620
- package/dist/session-namer.js +66 -17
- package/dist/terminals.js +94 -27
- package/dist/timeline.js +185 -0
- package/dist/voice.js +153 -35
- package/dist/workspaces.js +91 -26
- package/dist/ws/agent-socket.js +256 -0
- package/dist/ws/terminal-socket.js +46 -0
- package/npm-shrinkwrap.json +3685 -0
- package/package.json +11 -1
- package/packages/protocol/openapi.yaml +6 -2
- package/packages/protocol/websocket.schema.json +58 -15
- package/web/activity-view.js +121 -0
- package/web/app.js +3098 -743
- package/web/backend-state.js +59 -27
- package/web/collab.js +668 -255
- package/web/hid-keyboard.js +173 -140
- package/web/index.html +4 -1
- package/web/styles.css +14 -0
package/dist/collab/store.js
CHANGED
|
@@ -2,14 +2,20 @@ import { randomUUID, createHash } from 'node:crypto';
|
|
|
2
2
|
import { COLLAB_ERRORS, DEFAULT_POLICY } from './types.js';
|
|
3
3
|
const now = () => Date.now();
|
|
4
4
|
const iso = (value) => new Date(value).toISOString();
|
|
5
|
-
const asTime = (value) => {
|
|
6
|
-
const
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
const asTime = (value) => {
|
|
6
|
+
const time = typeof value === 'number' ? value : Date.parse(value ?? '');
|
|
7
|
+
return Number.isFinite(time) ? time : now();
|
|
8
|
+
};
|
|
9
|
+
const json = (value, fallback) => {
|
|
10
|
+
if (value === null || value === undefined)
|
|
11
|
+
return fallback;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(value);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
13
19
|
export const hashToken = (token) => createHash('sha256').update(token).digest('hex');
|
|
14
20
|
const notFound = (code, message) => Object.assign(new Error(message), { code });
|
|
15
21
|
/**
|
|
@@ -35,7 +41,9 @@ export class CollabStore {
|
|
|
35
41
|
constructor(connection) {
|
|
36
42
|
this.connection = connection;
|
|
37
43
|
}
|
|
38
|
-
get db() {
|
|
44
|
+
get db() {
|
|
45
|
+
return this.connection();
|
|
46
|
+
}
|
|
39
47
|
init() {
|
|
40
48
|
this.db.exec(`
|
|
41
49
|
CREATE TABLE IF NOT EXISTS collab_sessions (
|
|
@@ -152,7 +160,8 @@ export class CollabStore {
|
|
|
152
160
|
const issueColumns = this.db.prepare('PRAGMA table_info(collab_issues)').all();
|
|
153
161
|
if (!issueColumns.some(column => column.name === 'issue_number'))
|
|
154
162
|
this.db.exec('ALTER TABLE collab_issues ADD COLUMN issue_number INTEGER');
|
|
155
|
-
this.db
|
|
163
|
+
this.db
|
|
164
|
+
.exec(`UPDATE collab_issues AS current SET issue_number=(SELECT COUNT(*) FROM collab_issues AS older WHERE older.session_id=current.session_id AND (older.created_at<current.created_at OR (older.created_at=current.created_at AND older.rowid<=current.rowid))) WHERE issue_number IS NULL;
|
|
156
165
|
CREATE UNIQUE INDEX IF NOT EXISTS collab_issues_session_number ON collab_issues(session_id,issue_number);`);
|
|
157
166
|
// Additive migration: a commit range is pinned by its resolved endpoints, not by its spelling.
|
|
158
167
|
const baselineColumns = this.db.prepare('PRAGMA table_info(collab_baselines)').all();
|
|
@@ -172,7 +181,9 @@ export class CollabStore {
|
|
|
172
181
|
try {
|
|
173
182
|
this.db.exec('ALTER TABLE collab_participants DROP COLUMN dispatch_token');
|
|
174
183
|
}
|
|
175
|
-
catch {
|
|
184
|
+
catch {
|
|
185
|
+
/* column stays, always NULL */
|
|
186
|
+
}
|
|
176
187
|
}
|
|
177
188
|
// Rebuild migration: `binding_type` is gone (every seat is a local agent now) and it was NOT NULL, so an
|
|
178
189
|
// insert against the new column list would fail on a database created before that decision. A legacy
|
|
@@ -190,13 +201,16 @@ export class CollabStore {
|
|
|
190
201
|
if (Number(this.db.pragma('user_version', { simple: true })) < 4)
|
|
191
202
|
this.db.pragma('user_version = 4');
|
|
192
203
|
}
|
|
193
|
-
transaction(work) {
|
|
204
|
+
transaction(work) {
|
|
205
|
+
return this.db.transaction(work)();
|
|
206
|
+
}
|
|
194
207
|
// ---- sessions ----
|
|
195
208
|
createSession(input) {
|
|
196
209
|
const sessionId = `collab-${randomUUID()}`, timestamp = now();
|
|
197
210
|
const policy = mergePolicy(input.policy);
|
|
198
211
|
const phase = input.kind === 'review' ? 'draft' : 'nominating';
|
|
199
|
-
this.db
|
|
212
|
+
this.db
|
|
213
|
+
.prepare(`INSERT INTO collab_sessions(id,kind,title,workspace_id,cwd,subject_json,phase,round,policy_json,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,1,?,'active',?,?)`)
|
|
200
214
|
.run(sessionId, input.kind, input.title, input.workspaceId, input.cwd, JSON.stringify(input.subject), phase, JSON.stringify(policy), timestamp, timestamp);
|
|
201
215
|
return this.getSession(sessionId);
|
|
202
216
|
}
|
|
@@ -206,7 +220,10 @@ export class CollabStore {
|
|
|
206
220
|
throw notFound(COLLAB_ERRORS.sessionNotFound, 'Collaboration session not found');
|
|
207
221
|
return sessionFrom(row);
|
|
208
222
|
}
|
|
209
|
-
findSession(sessionId) {
|
|
223
|
+
findSession(sessionId) {
|
|
224
|
+
const row = this.db.prepare('SELECT * FROM collab_sessions WHERE id=?').get(sessionId);
|
|
225
|
+
return row ? sessionFrom(row) : undefined;
|
|
226
|
+
}
|
|
210
227
|
listSessions(filter = {}) {
|
|
211
228
|
const where = [], params = [];
|
|
212
229
|
if (filter.status) {
|
|
@@ -223,7 +240,21 @@ export class CollabStore {
|
|
|
223
240
|
}
|
|
224
241
|
updateSession(sessionId, patch) {
|
|
225
242
|
const current = this.getSession(sessionId);
|
|
226
|
-
this.db
|
|
243
|
+
this.db
|
|
244
|
+
.prepare('UPDATE collab_sessions SET phase=?,round=?,debate_round=?,status=?,stalled_json=?,outcome_json=?,policy_json=?,title=?,updated_at=? WHERE id=?')
|
|
245
|
+
.run(patch.phase ?? current.phase, patch.round ?? current.round, patch.debateRound ?? current.debateRound, patch.status ?? current.status, 'stalled' in patch
|
|
246
|
+
? patch.stalled
|
|
247
|
+
? JSON.stringify(patch.stalled)
|
|
248
|
+
: null
|
|
249
|
+
: current.stalled
|
|
250
|
+
? JSON.stringify(current.stalled)
|
|
251
|
+
: null, 'outcome' in patch
|
|
252
|
+
? patch.outcome
|
|
253
|
+
? JSON.stringify(patch.outcome)
|
|
254
|
+
: null
|
|
255
|
+
: current.outcome
|
|
256
|
+
? JSON.stringify(current.outcome)
|
|
257
|
+
: null, JSON.stringify(patch.policy ?? current.policy), patch.title ?? current.title, now(), sessionId);
|
|
227
258
|
return this.getSession(sessionId);
|
|
228
259
|
}
|
|
229
260
|
// ---- participants ----
|
|
@@ -231,7 +262,8 @@ export class CollabStore {
|
|
|
231
262
|
createParticipant(input) {
|
|
232
263
|
const participantId = `p-${randomUUID()}`, token = `cpt_${randomUUID().replace(/-/g, '')}${randomUUID().replace(/-/g, '')}`, timestamp = now();
|
|
233
264
|
const session = this.getSession(input.sessionId);
|
|
234
|
-
this.db
|
|
265
|
+
this.db
|
|
266
|
+
.prepare(`INSERT INTO collab_participants(id,session_id,role,display_name,model,agent_id,token_hash,state,token_budget,tokens_used,tokens_estimated,created_at) VALUES(?,?,?,?,?,?,?,'active',?,0,0,?)`)
|
|
235
267
|
.run(participantId, input.sessionId, input.role, input.displayName, input.model ?? null, input.agentId, hashToken(token), input.tokenBudget ?? session.policy.tokenBudgetPerParticipant, timestamp);
|
|
236
268
|
return { participant: this.getParticipant(participantId), token };
|
|
237
269
|
}
|
|
@@ -242,58 +274,98 @@ export class CollabStore {
|
|
|
242
274
|
return participantFrom(row);
|
|
243
275
|
}
|
|
244
276
|
findParticipantByToken(token) {
|
|
245
|
-
const row = this.db
|
|
277
|
+
const row = this.db
|
|
278
|
+
.prepare('SELECT * FROM collab_participants WHERE token_hash=?')
|
|
279
|
+
.get(hashToken(token));
|
|
246
280
|
return row ? participantFrom(row) : undefined;
|
|
247
281
|
}
|
|
248
|
-
listParticipants(sessionId) {
|
|
249
|
-
|
|
282
|
+
listParticipants(sessionId) {
|
|
283
|
+
return this.db
|
|
284
|
+
.prepare('SELECT * FROM collab_participants WHERE session_id=? ORDER BY created_at, rowid')
|
|
285
|
+
.all(sessionId).map(participantFrom);
|
|
286
|
+
}
|
|
287
|
+
findActiveParticipantsByAgent(agentId) {
|
|
288
|
+
return this.db
|
|
289
|
+
.prepare(`SELECT p.* FROM collab_participants p JOIN collab_sessions s ON s.id=p.session_id WHERE p.agent_id=? AND p.state='active' AND s.status='active' ORDER BY p.created_at,p.rowid`)
|
|
290
|
+
.all(agentId).map(participantFrom);
|
|
291
|
+
}
|
|
250
292
|
/**
|
|
251
293
|
* Seats that still own the Agent. A `budget_exhausted` seat is included because raising its budget
|
|
252
294
|
* reactivates it; letting the same Agent take a second seat meanwhile would make the bridge ambiguous.
|
|
253
295
|
*/
|
|
254
|
-
findOccupyingParticipantsByAgent(agentId) {
|
|
296
|
+
findOccupyingParticipantsByAgent(agentId) {
|
|
297
|
+
return this.db
|
|
298
|
+
.prepare(`SELECT p.* FROM collab_participants p JOIN collab_sessions s ON s.id=p.session_id WHERE p.agent_id=? AND p.state<>'left' AND s.status='active' ORDER BY p.created_at,p.rowid`)
|
|
299
|
+
.all(agentId).map(participantFrom);
|
|
300
|
+
}
|
|
255
301
|
/**
|
|
256
302
|
* Hands the seat to another local agent. The token is rotated because the old one is already in the old
|
|
257
303
|
* agent's conversation; only its hash is stored, and the plaintext is returned to the human exactly once.
|
|
258
304
|
*/
|
|
259
305
|
rebindParticipant(participantId, agentId, model) {
|
|
260
306
|
const token = `cpt_${randomUUID().replace(/-/g, '')}${randomUUID().replace(/-/g, '')}`;
|
|
261
|
-
this.db
|
|
307
|
+
this.db
|
|
308
|
+
.prepare('UPDATE collab_participants SET agent_id=?,model=?,token_hash=? WHERE id=?')
|
|
262
309
|
.run(agentId, model ?? null, hashToken(token), participantId);
|
|
263
310
|
return { participant: this.getParticipant(participantId), token };
|
|
264
311
|
}
|
|
265
312
|
/** Reopening a finished review invalidates the old bearer token before any Agent is called again. */
|
|
266
313
|
rotateParticipantToken(participantId) {
|
|
267
314
|
const token = `cpt_${randomUUID().replace(/-/g, '')}${randomUUID().replace(/-/g, '')}`;
|
|
268
|
-
this.db
|
|
315
|
+
this.db
|
|
316
|
+
.prepare('UPDATE collab_participants SET token_hash=? WHERE id=?')
|
|
317
|
+
.run(hashToken(token), participantId);
|
|
269
318
|
return token;
|
|
270
319
|
}
|
|
271
320
|
updateParticipant(participantId, patch) {
|
|
272
321
|
const current = this.getParticipant(participantId);
|
|
273
|
-
this.db
|
|
322
|
+
this.db
|
|
323
|
+
.prepare('UPDATE collab_participants SET state=?,tokens_used=?,token_budget=?,tokens_estimated=?,last_seen_at=?,model=? WHERE id=?')
|
|
324
|
+
.run(patch.state ?? current.state, Math.max(0, Math.round(patch.tokensUsed ?? current.tokensUsed)), Math.max(0, Math.round(patch.tokenBudget ?? current.tokenBudget)), (patch.tokensEstimated ?? current.tokensEstimated) ? 1 : 0, patch.lastSeenAt ? asTime(patch.lastSeenAt) : current.lastSeenAt ? asTime(current.lastSeenAt) : null, patch.model ?? current.model ?? null, participantId);
|
|
274
325
|
return this.getParticipant(participantId);
|
|
275
326
|
}
|
|
276
327
|
/** Adds usage and flips the participant to `budget_exhausted` once the budget is spent. Returns the updated row. */
|
|
277
328
|
addTokenUsage(participantId, tokens, estimated) {
|
|
278
329
|
const current = this.getParticipant(participantId), used = current.tokensUsed + Math.max(0, Math.round(tokens));
|
|
279
|
-
return this.updateParticipant(participantId, {
|
|
330
|
+
return this.updateParticipant(participantId, {
|
|
331
|
+
tokensUsed: used,
|
|
332
|
+
tokensEstimated: current.tokensEstimated || estimated,
|
|
333
|
+
state: current.state === 'left' ? 'left' : used >= current.tokenBudget ? 'budget_exhausted' : 'active'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
getTokenBaseline(participantId) {
|
|
337
|
+
const row = this.db
|
|
338
|
+
.prepare('SELECT token_baseline FROM collab_participants WHERE id=?')
|
|
339
|
+
.get(participantId);
|
|
340
|
+
return row?.token_baseline ?? undefined;
|
|
341
|
+
}
|
|
342
|
+
setTokenBaseline(participantId, value) {
|
|
343
|
+
this.db
|
|
344
|
+
.prepare('UPDATE collab_participants SET token_baseline=? WHERE id=?')
|
|
345
|
+
.run(Math.max(0, Math.round(value)), participantId);
|
|
280
346
|
}
|
|
281
|
-
getTokenBaseline(participantId) { const row = this.db.prepare('SELECT token_baseline FROM collab_participants WHERE id=?').get(participantId); return row?.token_baseline ?? undefined; }
|
|
282
|
-
setTokenBaseline(participantId, value) { this.db.prepare('UPDATE collab_participants SET token_baseline=? WHERE id=?').run(Math.max(0, Math.round(value)), participantId); }
|
|
283
347
|
// ---- events ----
|
|
284
348
|
appendEvent(sessionId, type, payload = {}, actorId) {
|
|
285
|
-
const row = this.db
|
|
349
|
+
const row = this.db
|
|
350
|
+
.prepare('SELECT COALESCE(MAX(sequence),0) AS last FROM collab_events WHERE session_id=?')
|
|
351
|
+
.get(sessionId);
|
|
286
352
|
const sequence = Number(row?.last ?? 0) + 1, eventId = `e-${randomUUID()}`, timestamp = now();
|
|
287
|
-
this.db
|
|
353
|
+
this.db
|
|
354
|
+
.prepare('INSERT INTO collab_events(session_id,sequence,id,type,actor_id,payload_json,created_at) VALUES(?,?,?,?,?,?,?)')
|
|
355
|
+
.run(sessionId, sequence, eventId, type, actorId ?? null, JSON.stringify(payload), timestamp);
|
|
288
356
|
return { sessionId, sequence, eventId, type, actorId, payload, createdAt: iso(timestamp) };
|
|
289
357
|
}
|
|
290
358
|
listEvents(sessionId, since = 0, limit = 500) {
|
|
291
|
-
const rows = this.db
|
|
359
|
+
const rows = this.db
|
|
360
|
+
.prepare('SELECT * FROM collab_events WHERE session_id=? AND sequence>? ORDER BY sequence LIMIT ?')
|
|
361
|
+
.all(sessionId, Math.max(0, Math.trunc(since)), Math.min(2000, Math.max(1, Math.trunc(limit))));
|
|
292
362
|
return rows.map(eventFrom);
|
|
293
363
|
}
|
|
294
364
|
/** The newest `limit` events, still returned oldest-first. A UI that wants "the latest" cannot page from 0. */
|
|
295
365
|
listRecentEvents(sessionId, limit = 200) {
|
|
296
|
-
const rows = this.db
|
|
366
|
+
const rows = this.db
|
|
367
|
+
.prepare('SELECT * FROM collab_events WHERE session_id=? ORDER BY sequence DESC LIMIT ?')
|
|
368
|
+
.all(sessionId, Math.min(2000, Math.max(1, Math.trunc(limit))));
|
|
297
369
|
return rows.reverse().map(eventFrom);
|
|
298
370
|
}
|
|
299
371
|
/**
|
|
@@ -304,24 +376,38 @@ export class CollabStore {
|
|
|
304
376
|
listEventsByType(sessionId, types) {
|
|
305
377
|
if (!types.length)
|
|
306
378
|
return [];
|
|
307
|
-
const rows = this.db
|
|
379
|
+
const rows = this.db
|
|
380
|
+
.prepare(`SELECT * FROM collab_events WHERE session_id=? AND type IN (${types.map(() => '?').join(',')}) ORDER BY sequence`)
|
|
381
|
+
.all(sessionId, ...types);
|
|
308
382
|
return rows.map(eventFrom);
|
|
309
383
|
}
|
|
310
384
|
// ---- baselines ----
|
|
311
385
|
saveBaseline(input) {
|
|
312
386
|
const baselineId = input.baselineId ?? `b-${randomUUID()}`, timestamp = now();
|
|
313
|
-
this.db
|
|
387
|
+
this.db
|
|
388
|
+
.prepare(`INSERT INTO collab_baselines(id,session_id,round,vcs,commit_sha,range_expr,range_resolved,dirty_hash,paths_json,captured_at) VALUES(?,?,?,?,?,?,?,?,?,?)
|
|
314
389
|
ON CONFLICT(session_id,round) DO UPDATE SET vcs=excluded.vcs,commit_sha=excluded.commit_sha,range_expr=excluded.range_expr,range_resolved=excluded.range_resolved,dirty_hash=excluded.dirty_hash,paths_json=excluded.paths_json,captured_at=excluded.captured_at`)
|
|
315
390
|
.run(baselineId, input.sessionId, input.round, input.vcs, input.commit ?? null, input.range ?? null, input.rangeResolved ?? null, input.dirtyHash ?? null, JSON.stringify(input.paths ?? []), timestamp);
|
|
316
391
|
return this.getBaselineForRound(input.sessionId, input.round);
|
|
317
392
|
}
|
|
318
|
-
getBaseline(baselineId) {
|
|
319
|
-
|
|
393
|
+
getBaseline(baselineId) {
|
|
394
|
+
const row = this.db.prepare('SELECT * FROM collab_baselines WHERE id=?').get(baselineId);
|
|
395
|
+
return row ? baselineFrom(row) : undefined;
|
|
396
|
+
}
|
|
397
|
+
getBaselineForRound(sessionId, round) {
|
|
398
|
+
const row = this.db
|
|
399
|
+
.prepare('SELECT * FROM collab_baselines WHERE session_id=? AND round=?')
|
|
400
|
+
.get(sessionId, round);
|
|
401
|
+
return row ? baselineFrom(row) : undefined;
|
|
402
|
+
}
|
|
320
403
|
// ---- issues ----
|
|
321
404
|
createIssue(input) {
|
|
322
405
|
const issueId = `i-${randomUUID()}`, timestamp = now();
|
|
323
|
-
const issueNumber = Number(this.db
|
|
324
|
-
|
|
406
|
+
const issueNumber = Number(this.db
|
|
407
|
+
.prepare('SELECT COALESCE(MAX(issue_number),0)+1 AS next FROM collab_issues WHERE session_id=?')
|
|
408
|
+
.get(input.sessionId).next);
|
|
409
|
+
this.db
|
|
410
|
+
.prepare(`INSERT INTO collab_issues(id,issue_number,session_id,external_id,reporter_id,target_participant_id,title,severity,category,required_action,confidence,location_json,evidence,impact,suggestion,baseline_id,status,round,version,merged_into,created_at,updated_at)
|
|
325
411
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,NULL,?,?)`)
|
|
326
412
|
.run(issueId, issueNumber, input.sessionId, input.externalId ?? null, input.reporterId, input.targetParticipantId, input.title, input.severity, input.category, input.requiredAction, input.confidence ?? null, JSON.stringify(input.location), input.evidence ?? null, input.impact ?? null, input.suggestion ?? null, input.baselineId, input.status ?? 'open', input.round, timestamp, timestamp);
|
|
327
413
|
return this.getIssue(issueId);
|
|
@@ -334,7 +420,9 @@ export class CollabStore {
|
|
|
334
420
|
}
|
|
335
421
|
/** Scoped lookup: an issue id from another session must read as "not found", never leak across sessions. */
|
|
336
422
|
findIssueInSession(sessionId, issueId) {
|
|
337
|
-
const row = this.db
|
|
423
|
+
const row = this.db
|
|
424
|
+
.prepare('SELECT * FROM collab_issues WHERE id=? AND session_id=?')
|
|
425
|
+
.get(issueId, sessionId);
|
|
338
426
|
return row ? issueFrom(row) : undefined;
|
|
339
427
|
}
|
|
340
428
|
listIssues(sessionId, filter = {}) {
|
|
@@ -355,57 +443,124 @@ export class CollabStore {
|
|
|
355
443
|
where.push('round=?');
|
|
356
444
|
params.push(filter.round);
|
|
357
445
|
}
|
|
358
|
-
return this.db
|
|
446
|
+
return this.db
|
|
447
|
+
.prepare(`SELECT * FROM collab_issues WHERE ${where.join(' AND ')} ORDER BY created_at, rowid`)
|
|
448
|
+
.all(...params).map(issueFrom);
|
|
359
449
|
}
|
|
360
450
|
/** Optimistic locking: a stale `expectedVersion` means someone else already moved the issue. */
|
|
361
451
|
updateIssue(issueId, patch, expectedVersion) {
|
|
362
452
|
const current = this.getIssue(issueId);
|
|
363
453
|
if (expectedVersion !== undefined && expectedVersion !== current.version)
|
|
364
454
|
throw Object.assign(new Error(`Issue was modified concurrently (expected version ${expectedVersion}, current ${current.version})`), { code: COLLAB_ERRORS.conflict, currentVersion: current.version });
|
|
365
|
-
this.db
|
|
455
|
+
this.db
|
|
456
|
+
.prepare('UPDATE collab_issues SET status=?,round=?,merged_into=?,severity=?,required_action=?,version=version+1,updated_at=? WHERE id=?')
|
|
457
|
+
.run(patch.status ?? current.status, patch.round ?? current.round, 'mergedInto' in patch ? (patch.mergedInto ?? null) : (current.mergedInto ?? null), patch.severity ?? current.severity, patch.requiredAction ?? current.requiredAction, now(), issueId);
|
|
366
458
|
return this.getIssue(issueId);
|
|
367
459
|
}
|
|
368
460
|
addIssueMessage(issueId, round, authorId, kind, payload) {
|
|
369
461
|
const messageId = `im-${randomUUID()}`, timestamp = now();
|
|
370
|
-
this.db
|
|
462
|
+
this.db
|
|
463
|
+
.prepare('INSERT INTO collab_issue_messages(id,issue_id,round,author_id,kind,payload_json,created_at) VALUES(?,?,?,?,?,?,?)')
|
|
464
|
+
.run(messageId, issueId, round, authorId, kind, JSON.stringify(payload), timestamp);
|
|
371
465
|
return { messageId, issueId, round, authorId, kind, payload, createdAt: iso(timestamp) };
|
|
372
466
|
}
|
|
373
467
|
listIssueMessages(issueId) {
|
|
374
|
-
return this.db
|
|
375
|
-
.
|
|
468
|
+
return this.db
|
|
469
|
+
.prepare('SELECT * FROM collab_issue_messages WHERE issue_id=? ORDER BY created_at, rowid')
|
|
470
|
+
.all(issueId).map(row => ({
|
|
471
|
+
messageId: row.id,
|
|
472
|
+
issueId: row.issue_id,
|
|
473
|
+
round: row.round,
|
|
474
|
+
authorId: row.author_id,
|
|
475
|
+
kind: row.kind,
|
|
476
|
+
payload: json(row.payload_json, {}),
|
|
477
|
+
createdAt: iso(row.created_at)
|
|
478
|
+
}));
|
|
376
479
|
}
|
|
377
480
|
// ---- review consensus votes and duplicate-merge proposals ----
|
|
378
481
|
saveIssueVote(input) {
|
|
379
|
-
this.db
|
|
482
|
+
this.db
|
|
483
|
+
.prepare(`INSERT INTO collab_issue_votes(id,session_id,issue_id,participant_id,round,consensus_round,stance,rationale,created_at) VALUES(?,?,?,?,?,?,?,?,?)
|
|
380
484
|
ON CONFLICT(issue_id,participant_id,round,consensus_round) DO UPDATE SET stance=excluded.stance,rationale=excluded.rationale,created_at=excluded.created_at`)
|
|
381
485
|
.run(`iv-${randomUUID()}`, input.sessionId, input.issueId, input.participantId, input.round, input.consensusRound, input.stance, input.rationale ?? null, now());
|
|
382
|
-
return this.listIssueVotes(input.sessionId).find(vote => vote.issueId === input.issueId &&
|
|
486
|
+
return this.listIssueVotes(input.sessionId).find(vote => vote.issueId === input.issueId &&
|
|
487
|
+
vote.participantId === input.participantId &&
|
|
488
|
+
vote.round === input.round &&
|
|
489
|
+
vote.consensusRound === input.consensusRound);
|
|
490
|
+
}
|
|
491
|
+
listIssueVotes(sessionId) {
|
|
492
|
+
return this.db
|
|
493
|
+
.prepare('SELECT * FROM collab_issue_votes WHERE session_id=? ORDER BY created_at,rowid')
|
|
494
|
+
.all(sessionId).map(row => ({
|
|
495
|
+
voteId: row.id,
|
|
496
|
+
sessionId: row.session_id,
|
|
497
|
+
issueId: row.issue_id,
|
|
498
|
+
participantId: row.participant_id,
|
|
499
|
+
round: row.round,
|
|
500
|
+
consensusRound: row.consensus_round,
|
|
501
|
+
stance: row.stance,
|
|
502
|
+
rationale: row.rationale ?? undefined,
|
|
503
|
+
createdAt: iso(row.created_at)
|
|
504
|
+
}));
|
|
383
505
|
}
|
|
384
|
-
listIssueVotes(sessionId) { return this.db.prepare('SELECT * FROM collab_issue_votes WHERE session_id=? ORDER BY created_at,rowid').all(sessionId).map(row => ({ voteId: row.id, sessionId: row.session_id, issueId: row.issue_id, participantId: row.participant_id, round: row.round, consensusRound: row.consensus_round, stance: row.stance, rationale: row.rationale ?? undefined, createdAt: iso(row.created_at) })); }
|
|
385
506
|
createMergeProposal(input) {
|
|
386
507
|
const issueIds = [...new Set(input.issueIds)].sort(), encoded = JSON.stringify(issueIds);
|
|
387
|
-
let row = this.db
|
|
508
|
+
let row = this.db
|
|
509
|
+
.prepare('SELECT id FROM collab_merge_proposals WHERE session_id=? AND round=? AND issue_ids_json=?')
|
|
510
|
+
.get(input.sessionId, input.round, encoded);
|
|
388
511
|
if (!row) {
|
|
389
512
|
const id = `mp-${randomUUID()}`;
|
|
390
|
-
this.db
|
|
513
|
+
this.db
|
|
514
|
+
.prepare('INSERT INTO collab_merge_proposals(id,session_id,round,issue_ids_json,rationale,created_at) VALUES(?,?,?,?,?,?)')
|
|
515
|
+
.run(id, input.sessionId, input.round, encoded, input.rationale, now());
|
|
391
516
|
row = { id };
|
|
392
517
|
}
|
|
393
|
-
this.saveMergeVote({
|
|
518
|
+
this.saveMergeVote({
|
|
519
|
+
proposalId: row.id,
|
|
520
|
+
participantId: input.participantId,
|
|
521
|
+
stance: 'approve',
|
|
522
|
+
rationale: input.rationale,
|
|
523
|
+
proposer: true
|
|
524
|
+
});
|
|
394
525
|
return this.listMergeProposals(input.sessionId).find(entry => entry.proposalId === row.id);
|
|
395
526
|
}
|
|
396
527
|
saveMergeVote(input) {
|
|
397
|
-
this.db
|
|
528
|
+
this.db
|
|
529
|
+
.prepare(`INSERT INTO collab_merge_votes(id,proposal_id,participant_id,stance,rationale,proposer,created_at) VALUES(?,?,?,?,?,?,?)
|
|
398
530
|
ON CONFLICT(proposal_id,participant_id) DO UPDATE SET stance=excluded.stance,rationale=excluded.rationale,proposer=MAX(collab_merge_votes.proposer,excluded.proposer),created_at=excluded.created_at`)
|
|
399
531
|
.run(`mv-${randomUUID()}`, input.proposalId, input.participantId, input.stance, input.rationale ?? null, input.proposer ? 1 : 0, now());
|
|
400
532
|
}
|
|
401
533
|
listMergeProposals(sessionId) {
|
|
402
|
-
const proposals = this.db
|
|
403
|
-
|
|
534
|
+
const proposals = this.db
|
|
535
|
+
.prepare('SELECT * FROM collab_merge_proposals WHERE session_id=? ORDER BY created_at,rowid')
|
|
536
|
+
.all(sessionId), votes = this.db
|
|
537
|
+
.prepare('SELECT v.* FROM collab_merge_votes v JOIN collab_merge_proposals p ON p.id=v.proposal_id WHERE p.session_id=? ORDER BY v.created_at,v.rowid')
|
|
538
|
+
.all(sessionId);
|
|
539
|
+
return proposals.map(row => ({
|
|
540
|
+
proposalId: row.id,
|
|
541
|
+
sessionId: row.session_id,
|
|
542
|
+
round: row.round,
|
|
543
|
+
issueIds: json(row.issue_ids_json, []),
|
|
544
|
+
rationale: row.rationale,
|
|
545
|
+
createdAt: iso(row.created_at),
|
|
546
|
+
votes: votes
|
|
547
|
+
.filter(vote => vote.proposal_id === row.id)
|
|
548
|
+
.map(vote => ({
|
|
549
|
+
voteId: vote.id,
|
|
550
|
+
proposalId: vote.proposal_id,
|
|
551
|
+
participantId: vote.participant_id,
|
|
552
|
+
stance: vote.stance,
|
|
553
|
+
rationale: vote.rationale ?? undefined,
|
|
554
|
+
proposer: !!vote.proposer,
|
|
555
|
+
createdAt: iso(vote.created_at)
|
|
556
|
+
}))
|
|
557
|
+
}));
|
|
404
558
|
}
|
|
405
559
|
// ---- escalations ----
|
|
406
560
|
createEscalation(input) {
|
|
407
561
|
const escalationId = `esc-${randomUUID()}`, timestamp = now();
|
|
408
|
-
this.db
|
|
562
|
+
this.db
|
|
563
|
+
.prepare(`INSERT INTO collab_escalations(id,session_id,kind,ref_id,raised_by,summary,positions_json,question,options_json,urgency,status,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,'pending',?)`)
|
|
409
564
|
.run(escalationId, input.sessionId, input.kind, input.refId ?? null, input.raisedBy, input.summary, JSON.stringify(input.positions), input.question, JSON.stringify(input.options), input.urgency, timestamp);
|
|
410
565
|
return this.getEscalation(escalationId);
|
|
411
566
|
}
|
|
@@ -426,34 +581,59 @@ export class CollabStore {
|
|
|
426
581
|
params.push(filter.status);
|
|
427
582
|
}
|
|
428
583
|
const limit = Math.min(200, Math.max(1, Math.trunc(filter.limit ?? 100)));
|
|
429
|
-
return this.db
|
|
584
|
+
return this.db
|
|
585
|
+
.prepare(`SELECT * FROM collab_escalations ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY created_at DESC, id DESC LIMIT ?`)
|
|
586
|
+
.all(...params, limit).map(escalationFrom);
|
|
430
587
|
}
|
|
431
588
|
findPendingEscalation(sessionId, kind, refId) {
|
|
432
|
-
const row = this.db
|
|
589
|
+
const row = this.db
|
|
590
|
+
.prepare(`SELECT * FROM collab_escalations WHERE session_id=? AND kind=? AND status='pending' AND ${refId ? 'ref_id=?' : 'ref_id IS NULL'}`)
|
|
591
|
+
.get(...(refId ? [sessionId, kind, refId] : [sessionId, kind]));
|
|
433
592
|
return row ? escalationFrom(row) : undefined;
|
|
434
593
|
}
|
|
435
594
|
resolveEscalation(escalationId, decision, resolvedBy, status = 'resolved') {
|
|
436
595
|
this.getEscalation(escalationId);
|
|
437
|
-
this.db
|
|
596
|
+
this.db
|
|
597
|
+
.prepare('UPDATE collab_escalations SET status=?,decision_json=?,resolved_by=?,resolved_at=? WHERE id=?')
|
|
598
|
+
.run(status, JSON.stringify(decision), resolvedBy, now(), escalationId);
|
|
438
599
|
return this.getEscalation(escalationId);
|
|
439
600
|
}
|
|
440
601
|
// ---- inbox: durable wake-up queue (acked when the extension collects normal work) ----
|
|
441
602
|
pushInbox(sessionId, participantId, type, payload) {
|
|
442
603
|
const itemId = `in-${randomUUID()}`, timestamp = now();
|
|
443
|
-
this.db
|
|
604
|
+
this.db
|
|
605
|
+
.prepare('INSERT INTO collab_inbox(id,session_id,participant_id,type,payload_json,created_at) VALUES(?,?,?,?,?,?)')
|
|
606
|
+
.run(itemId, sessionId, participantId, type, JSON.stringify(payload), timestamp);
|
|
444
607
|
return { itemId, sessionId, participantId, type, payload, createdAt: iso(timestamp) };
|
|
445
608
|
}
|
|
446
609
|
listInbox(participantId, includeAcked = false, limit = 50) {
|
|
447
|
-
const rows = this.db
|
|
448
|
-
|
|
610
|
+
const rows = this.db
|
|
611
|
+
.prepare(`SELECT * FROM collab_inbox WHERE participant_id=?${includeAcked ? '' : ' AND acked_at IS NULL'} ORDER BY created_at, rowid LIMIT ?`)
|
|
612
|
+
.all(participantId, Math.min(200, Math.max(1, limit)));
|
|
613
|
+
return rows.map(row => ({
|
|
614
|
+
itemId: row.id,
|
|
615
|
+
sessionId: row.session_id,
|
|
616
|
+
participantId: row.participant_id,
|
|
617
|
+
type: row.type,
|
|
618
|
+
payload: json(row.payload_json, {}),
|
|
619
|
+
createdAt: iso(row.created_at),
|
|
620
|
+
ackedAt: row.acked_at ? iso(row.acked_at) : undefined
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
ackInbox(participantId, itemIds) {
|
|
624
|
+
if (!itemIds.length)
|
|
625
|
+
return;
|
|
626
|
+
const timestamp = now(), update = this.db.prepare('UPDATE collab_inbox SET acked_at=? WHERE id=? AND participant_id=?');
|
|
627
|
+
this.db.transaction(() => {
|
|
628
|
+
for (const id of itemIds)
|
|
629
|
+
update.run(timestamp, id, participantId);
|
|
630
|
+
})();
|
|
449
631
|
}
|
|
450
|
-
ackInbox(participantId, itemIds) { if (!itemIds.length)
|
|
451
|
-
return; const timestamp = now(), update = this.db.prepare('UPDATE collab_inbox SET acked_at=? WHERE id=? AND participant_id=?'); this.db.transaction(() => { for (const id of itemIds)
|
|
452
|
-
update.run(timestamp, id, participantId); })(); }
|
|
453
632
|
// ---- scoring: criteria, votes, scores, debates ----
|
|
454
633
|
createCriterion(input) {
|
|
455
634
|
const criterionId = `cr-${randomUUID()}`;
|
|
456
|
-
this.db
|
|
635
|
+
this.db
|
|
636
|
+
.prepare('INSERT INTO collab_criteria(id,session_id,state,name,definition,anchors_json,weight,source_json,round,created_at) VALUES(?,?,?,?,?,?,?,?,?,?)')
|
|
457
637
|
.run(criterionId, input.sessionId, input.state ?? 'candidate', input.name, input.definition, input.anchors ? JSON.stringify(input.anchors) : null, input.weight ?? null, input.source ? JSON.stringify(input.source) : null, input.round, now());
|
|
458
638
|
return this.getCriterion(criterionId);
|
|
459
639
|
}
|
|
@@ -464,100 +644,275 @@ export class CollabStore {
|
|
|
464
644
|
return criterionFrom(row);
|
|
465
645
|
}
|
|
466
646
|
findCriterionInSession(sessionId, criterionId) {
|
|
467
|
-
const row = this.db
|
|
647
|
+
const row = this.db
|
|
648
|
+
.prepare('SELECT * FROM collab_criteria WHERE id=? AND session_id=?')
|
|
649
|
+
.get(criterionId, sessionId);
|
|
468
650
|
return row ? criterionFrom(row) : undefined;
|
|
469
651
|
}
|
|
470
|
-
listCriteria(sessionId) {
|
|
652
|
+
listCriteria(sessionId) {
|
|
653
|
+
return this.db
|
|
654
|
+
.prepare('SELECT * FROM collab_criteria WHERE session_id=? ORDER BY created_at, rowid')
|
|
655
|
+
.all(sessionId).map(criterionFrom);
|
|
656
|
+
}
|
|
471
657
|
updateCriterion(criterionId, patch) {
|
|
472
658
|
const current = this.getCriterion(criterionId);
|
|
473
|
-
this.db
|
|
474
|
-
.
|
|
659
|
+
this.db
|
|
660
|
+
.prepare('UPDATE collab_criteria SET state=?,weight=?,definition=?,name=?,anchors_json=?,source_json=? WHERE id=?')
|
|
661
|
+
.run(patch.state ?? current.state, patch.weight ?? current.weight ?? null, patch.definition ?? current.definition, patch.name ?? current.name, 'anchors' in patch
|
|
662
|
+
? patch.anchors
|
|
663
|
+
? JSON.stringify(patch.anchors)
|
|
664
|
+
: null
|
|
665
|
+
: current.anchors
|
|
666
|
+
? JSON.stringify(current.anchors)
|
|
667
|
+
: null, 'source' in patch
|
|
668
|
+
? patch.source
|
|
669
|
+
? JSON.stringify(patch.source)
|
|
670
|
+
: null
|
|
671
|
+
: current.source
|
|
672
|
+
? JSON.stringify(current.source)
|
|
673
|
+
: null, criterionId);
|
|
475
674
|
return this.getCriterion(criterionId);
|
|
476
675
|
}
|
|
477
676
|
saveVote(input) {
|
|
478
|
-
this.db
|
|
677
|
+
this.db
|
|
678
|
+
.prepare(`INSERT INTO collab_votes(id,session_id,criterion_id,participant_id,round,stance,weight,amendment,rationale,created_at) VALUES(?,?,?,?,?,?,?,?,?,?)
|
|
479
679
|
ON CONFLICT(criterion_id,participant_id,round) DO UPDATE SET stance=excluded.stance,weight=excluded.weight,amendment=excluded.amendment,rationale=excluded.rationale,created_at=excluded.created_at`)
|
|
480
680
|
.run(`v-${randomUUID()}`, input.sessionId, input.criterionId, input.participantId, input.round, input.stance, input.weight ?? null, input.amendment ?? null, input.rationale ?? null, now());
|
|
481
|
-
return this.listVotes(input.sessionId).find(vote => vote.criterionId === input.criterionId &&
|
|
681
|
+
return this.listVotes(input.sessionId).find(vote => vote.criterionId === input.criterionId &&
|
|
682
|
+
vote.participantId === input.participantId &&
|
|
683
|
+
vote.round === input.round);
|
|
482
684
|
}
|
|
483
685
|
listVotes(sessionId) {
|
|
484
|
-
return this.db
|
|
485
|
-
.
|
|
686
|
+
return this.db
|
|
687
|
+
.prepare('SELECT * FROM collab_votes WHERE session_id=? ORDER BY created_at, rowid')
|
|
688
|
+
.all(sessionId).map(row => ({
|
|
689
|
+
voteId: row.id,
|
|
690
|
+
sessionId: row.session_id,
|
|
691
|
+
criterionId: row.criterion_id,
|
|
692
|
+
participantId: row.participant_id,
|
|
693
|
+
round: row.round,
|
|
694
|
+
stance: row.stance,
|
|
695
|
+
weight: row.weight ?? undefined,
|
|
696
|
+
amendment: row.amendment ?? undefined,
|
|
697
|
+
rationale: row.rationale ?? undefined,
|
|
698
|
+
createdAt: iso(row.created_at)
|
|
699
|
+
}));
|
|
486
700
|
}
|
|
487
701
|
saveScore(input) {
|
|
488
|
-
this.db
|
|
702
|
+
this.db
|
|
703
|
+
.prepare(`INSERT INTO collab_scores(id,session_id,criterion_id,participant_id,round,score,rationale,evidence_json,confidence,change_reason,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
489
704
|
ON CONFLICT(criterion_id,participant_id,round) DO UPDATE SET score=excluded.score,rationale=excluded.rationale,evidence_json=excluded.evidence_json,confidence=excluded.confidence,change_reason=excluded.change_reason,created_at=excluded.created_at`)
|
|
490
705
|
.run(`s-${randomUUID()}`, input.sessionId, input.criterionId, input.participantId, input.round, input.score, input.rationale, JSON.stringify(input.evidence), input.confidence ?? null, input.changeReason ?? null, now());
|
|
491
|
-
return this.listScores(input.sessionId).find(score => score.criterionId === input.criterionId &&
|
|
706
|
+
return this.listScores(input.sessionId).find(score => score.criterionId === input.criterionId &&
|
|
707
|
+
score.participantId === input.participantId &&
|
|
708
|
+
score.round === input.round);
|
|
492
709
|
}
|
|
493
710
|
listScores(sessionId) {
|
|
494
|
-
return this.db
|
|
495
|
-
.
|
|
711
|
+
return this.db
|
|
712
|
+
.prepare('SELECT * FROM collab_scores WHERE session_id=? ORDER BY created_at, rowid')
|
|
713
|
+
.all(sessionId).map(row => ({
|
|
714
|
+
scoreId: row.id,
|
|
715
|
+
sessionId: row.session_id,
|
|
716
|
+
criterionId: row.criterion_id,
|
|
717
|
+
participantId: row.participant_id,
|
|
718
|
+
round: row.round,
|
|
719
|
+
score: row.score,
|
|
720
|
+
rationale: row.rationale,
|
|
721
|
+
evidence: json(row.evidence_json, []),
|
|
722
|
+
confidence: row.confidence ?? undefined,
|
|
723
|
+
changeReason: row.change_reason ?? undefined,
|
|
724
|
+
createdAt: iso(row.created_at)
|
|
725
|
+
}));
|
|
496
726
|
}
|
|
497
727
|
createDebate(sessionId, criterionId, round) {
|
|
498
728
|
const debateId = `d-${randomUUID()}`;
|
|
499
|
-
this.db
|
|
729
|
+
this.db
|
|
730
|
+
.prepare(`INSERT INTO collab_debates(id,session_id,criterion_id,round,status,created_at) VALUES(?,?,?,?,'open',?)`)
|
|
731
|
+
.run(debateId, sessionId, criterionId, round, now());
|
|
500
732
|
return this.listDebates(sessionId).find(debate => debate.debateId === debateId);
|
|
501
733
|
}
|
|
502
734
|
listDebates(sessionId) {
|
|
503
|
-
const rows = this.db
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
735
|
+
const rows = this.db
|
|
736
|
+
.prepare('SELECT * FROM collab_debates WHERE session_id=? ORDER BY created_at, rowid')
|
|
737
|
+
.all(sessionId);
|
|
738
|
+
return rows.map(row => ({
|
|
739
|
+
debateId: row.id,
|
|
740
|
+
sessionId: row.session_id,
|
|
741
|
+
criterionId: row.criterion_id,
|
|
742
|
+
round: row.round,
|
|
743
|
+
status: row.status,
|
|
744
|
+
createdAt: iso(row.created_at),
|
|
745
|
+
arguments: this.db
|
|
746
|
+
.prepare('SELECT * FROM collab_debate_arguments WHERE debate_id=? ORDER BY created_at, rowid')
|
|
747
|
+
.all(row.id).map(entry => ({
|
|
748
|
+
argumentId: entry.id,
|
|
749
|
+
debateId: entry.debate_id,
|
|
750
|
+
participantId: entry.participant_id,
|
|
751
|
+
stance: entry.stance,
|
|
752
|
+
argument: entry.argument,
|
|
753
|
+
evidence: json(entry.evidence_json, []),
|
|
754
|
+
respondingTo: entry.responding_to ?? undefined,
|
|
755
|
+
createdAt: iso(entry.created_at)
|
|
756
|
+
}))
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
findDebateInSession(sessionId, debateId) {
|
|
760
|
+
return this.listDebates(sessionId).find(debate => debate.debateId === debateId);
|
|
507
761
|
}
|
|
508
|
-
findDebateInSession(sessionId, debateId) { return this.listDebates(sessionId).find(debate => debate.debateId === debateId); }
|
|
509
762
|
addDebateArgument(debateId, input) {
|
|
510
763
|
const argumentId = `arg-${randomUUID()}`;
|
|
511
|
-
this.db
|
|
764
|
+
this.db
|
|
765
|
+
.prepare('INSERT INTO collab_debate_arguments(id,debate_id,participant_id,stance,argument,evidence_json,responding_to,created_at) VALUES(?,?,?,?,?,?,?,?)')
|
|
512
766
|
.run(argumentId, debateId, input.participantId, input.stance, input.argument, JSON.stringify(input.evidence ?? []), input.respondingTo ?? null, now());
|
|
513
767
|
return argumentId;
|
|
514
768
|
}
|
|
515
|
-
closeDebates(sessionId, round) {
|
|
769
|
+
closeDebates(sessionId, round) {
|
|
770
|
+
this.db
|
|
771
|
+
.prepare(`UPDATE collab_debates SET status='closed' WHERE session_id=? AND round=?`)
|
|
772
|
+
.run(sessionId, round);
|
|
773
|
+
}
|
|
516
774
|
// ---- phase completions ("I am done for this phase and round") ----
|
|
517
775
|
markPhaseComplete(sessionId, round, phase, participantId) {
|
|
518
|
-
this.db
|
|
776
|
+
this.db
|
|
777
|
+
.prepare('INSERT OR IGNORE INTO collab_phase_completions(session_id,round,phase,participant_id,created_at) VALUES(?,?,?,?,?)')
|
|
778
|
+
.run(sessionId, round, phase, participantId, now());
|
|
519
779
|
}
|
|
520
780
|
listCompletions(sessionId) {
|
|
521
|
-
return this.db.prepare('SELECT * FROM collab_phase_completions WHERE session_id=?').all(sessionId)
|
|
522
|
-
|
|
781
|
+
return this.db.prepare('SELECT * FROM collab_phase_completions WHERE session_id=?').all(sessionId).map(row => ({
|
|
782
|
+
sessionId: row.session_id,
|
|
783
|
+
round: row.round,
|
|
784
|
+
phase: row.phase,
|
|
785
|
+
participantId: row.participant_id
|
|
786
|
+
}));
|
|
523
787
|
}
|
|
524
788
|
// ---- idempotency ----
|
|
525
|
-
getIdempotent(key) {
|
|
526
|
-
|
|
789
|
+
getIdempotent(key) {
|
|
790
|
+
const row = this.db.prepare('SELECT response_json FROM collab_idempotency WHERE key=?').get(key);
|
|
791
|
+
return row ? json(row.response_json, undefined) : undefined;
|
|
792
|
+
}
|
|
793
|
+
saveIdempotent(key, sessionId, participantId, response) {
|
|
794
|
+
this.db
|
|
795
|
+
.prepare('INSERT OR REPLACE INTO collab_idempotency(key,session_id,participant_id,response_json,created_at) VALUES(?,?,?,?,?)')
|
|
796
|
+
.run(key, sessionId, participantId, JSON.stringify(response), now());
|
|
797
|
+
}
|
|
527
798
|
/** Idempotency records are a replay guard, not history: drop them once replays are implausible. */
|
|
528
|
-
pruneIdempotency(maxAgeMs = 7 * 24 * 3600 * 1000) {
|
|
799
|
+
pruneIdempotency(maxAgeMs = 7 * 24 * 3600 * 1000) {
|
|
800
|
+
this.db.prepare('DELETE FROM collab_idempotency WHERE created_at<?').run(now() - maxAgeMs);
|
|
801
|
+
}
|
|
529
802
|
}
|
|
530
803
|
export function mergePolicy(patch) {
|
|
531
|
-
return {
|
|
804
|
+
return {
|
|
805
|
+
...DEFAULT_POLICY,
|
|
806
|
+
...patch,
|
|
807
|
+
scoring: {
|
|
808
|
+
...DEFAULT_POLICY.scoring,
|
|
809
|
+
...patch?.scoring,
|
|
810
|
+
scale: { ...DEFAULT_POLICY.scoring.scale, ...patch?.scoring?.scale }
|
|
811
|
+
}
|
|
812
|
+
};
|
|
532
813
|
}
|
|
533
814
|
const sessionFrom = (row) => ({
|
|
534
|
-
sessionId: row.id,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
815
|
+
sessionId: row.id,
|
|
816
|
+
kind: row.kind,
|
|
817
|
+
title: row.title,
|
|
818
|
+
workspaceId: row.workspace_id,
|
|
819
|
+
cwd: row.cwd,
|
|
820
|
+
subject: json(row.subject_json, { type: 'free', value: '' }),
|
|
821
|
+
phase: row.phase,
|
|
822
|
+
round: row.round,
|
|
823
|
+
debateRound: row.debate_round ?? 0,
|
|
824
|
+
policy: mergePolicy(json(row.policy_json, {})),
|
|
825
|
+
status: row.status,
|
|
826
|
+
stalled: json(row.stalled_json, undefined),
|
|
827
|
+
outcome: json(row.outcome_json, undefined),
|
|
828
|
+
createdAt: iso(row.created_at),
|
|
829
|
+
updatedAt: iso(row.updated_at)
|
|
539
830
|
});
|
|
540
831
|
const participantFrom = (row) => ({
|
|
541
|
-
participantId: row.id,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
832
|
+
participantId: row.id,
|
|
833
|
+
sessionId: row.session_id,
|
|
834
|
+
role: row.role,
|
|
835
|
+
displayName: row.display_name,
|
|
836
|
+
model: row.model ?? undefined,
|
|
837
|
+
agentId: row.agent_id ?? '',
|
|
838
|
+
state: row.state,
|
|
839
|
+
tokenBudget: row.token_budget,
|
|
840
|
+
tokensUsed: row.tokens_used,
|
|
841
|
+
tokensEstimated: !!row.tokens_estimated,
|
|
842
|
+
createdAt: iso(row.created_at),
|
|
843
|
+
lastSeenAt: row.last_seen_at ? iso(row.last_seen_at) : undefined
|
|
844
|
+
});
|
|
845
|
+
const eventFrom = (row) => ({
|
|
846
|
+
sessionId: row.session_id,
|
|
847
|
+
sequence: row.sequence,
|
|
848
|
+
eventId: row.id,
|
|
849
|
+
type: row.type,
|
|
850
|
+
actorId: row.actor_id ?? undefined,
|
|
851
|
+
payload: json(row.payload_json, {}),
|
|
852
|
+
createdAt: iso(row.created_at)
|
|
853
|
+
});
|
|
854
|
+
const baselineFrom = (row) => ({
|
|
855
|
+
baselineId: row.id,
|
|
856
|
+
sessionId: row.session_id,
|
|
857
|
+
round: row.round,
|
|
858
|
+
vcs: row.vcs,
|
|
859
|
+
commit: row.commit_sha ?? undefined,
|
|
860
|
+
range: row.range_expr ?? undefined,
|
|
861
|
+
rangeResolved: row.range_resolved ?? undefined,
|
|
862
|
+
dirtyHash: row.dirty_hash ?? undefined,
|
|
863
|
+
paths: json(row.paths_json, []),
|
|
864
|
+
capturedAt: iso(row.captured_at)
|
|
545
865
|
});
|
|
546
|
-
const eventFrom = (row) => ({ sessionId: row.session_id, sequence: row.sequence, eventId: row.id, type: row.type, actorId: row.actor_id ?? undefined, payload: json(row.payload_json, {}), createdAt: iso(row.created_at) });
|
|
547
|
-
const baselineFrom = (row) => ({ baselineId: row.id, sessionId: row.session_id, round: row.round, vcs: row.vcs, commit: row.commit_sha ?? undefined, range: row.range_expr ?? undefined, rangeResolved: row.range_resolved ?? undefined, dirtyHash: row.dirty_hash ?? undefined, paths: json(row.paths_json, []), capturedAt: iso(row.captured_at) });
|
|
548
866
|
const issueFrom = (row) => ({
|
|
549
|
-
issueId: row.id,
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
867
|
+
issueId: row.id,
|
|
868
|
+
number: Number(row.issue_number ?? 0),
|
|
869
|
+
sessionId: row.session_id,
|
|
870
|
+
externalId: row.external_id ?? undefined,
|
|
871
|
+
reporterId: row.reporter_id,
|
|
872
|
+
targetParticipantId: row.target_participant_id,
|
|
873
|
+
title: row.title,
|
|
874
|
+
severity: row.severity,
|
|
875
|
+
category: row.category,
|
|
876
|
+
requiredAction: row.required_action,
|
|
877
|
+
confidence: row.confidence ?? undefined,
|
|
878
|
+
location: json(row.location_json, { path: '' }),
|
|
879
|
+
evidence: row.evidence ?? undefined,
|
|
880
|
+
impact: row.impact ?? undefined,
|
|
881
|
+
suggestion: row.suggestion ?? undefined,
|
|
882
|
+
baselineId: row.baseline_id,
|
|
883
|
+
status: row.status,
|
|
884
|
+
round: row.round,
|
|
885
|
+
version: row.version,
|
|
886
|
+
mergedInto: row.merged_into ?? undefined,
|
|
887
|
+
createdAt: iso(row.created_at),
|
|
888
|
+
updatedAt: iso(row.updated_at)
|
|
889
|
+
});
|
|
890
|
+
const criterionFrom = (row) => ({
|
|
891
|
+
criterionId: row.id,
|
|
892
|
+
sessionId: row.session_id,
|
|
893
|
+
state: row.state,
|
|
894
|
+
name: row.name,
|
|
895
|
+
definition: row.definition,
|
|
896
|
+
anchors: json(row.anchors_json, undefined),
|
|
897
|
+
weight: row.weight ?? undefined,
|
|
898
|
+
source: json(row.source_json, undefined),
|
|
899
|
+
round: row.round,
|
|
900
|
+
createdAt: iso(row.created_at)
|
|
554
901
|
});
|
|
555
|
-
const criterionFrom = (row) => ({ criterionId: row.id, sessionId: row.session_id, state: row.state, name: row.name, definition: row.definition,
|
|
556
|
-
anchors: json(row.anchors_json, undefined), weight: row.weight ?? undefined, source: json(row.source_json, undefined),
|
|
557
|
-
round: row.round, createdAt: iso(row.created_at) });
|
|
558
902
|
const escalationFrom = (row) => ({
|
|
559
|
-
escalationId: row.id,
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
903
|
+
escalationId: row.id,
|
|
904
|
+
sessionId: row.session_id,
|
|
905
|
+
kind: row.kind,
|
|
906
|
+
refId: row.ref_id ?? undefined,
|
|
907
|
+
raisedBy: row.raised_by,
|
|
908
|
+
summary: row.summary,
|
|
909
|
+
positions: json(row.positions_json, []),
|
|
910
|
+
question: row.question,
|
|
911
|
+
options: json(row.options_json, []),
|
|
912
|
+
urgency: row.urgency,
|
|
913
|
+
status: row.status,
|
|
914
|
+
decision: json(row.decision_json, undefined),
|
|
915
|
+
resolvedBy: row.resolved_by ?? undefined,
|
|
916
|
+
createdAt: iso(row.created_at),
|
|
917
|
+
resolvedAt: row.resolved_at ? iso(row.resolved_at) : undefined
|
|
563
918
|
});
|