ai_co_discuss 0.2.71 → 0.2.72
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/.agents/skills/discuss-host/references/manual.md +1 -1
- package/.agents/skills/discuss-participant/SKILL.md +1 -1
- package/.agents/skills/discuss-participant/references/free.md +2 -0
- package/backend/free_room.py +54 -5
- package/backend/schema.sql +11 -0
- package/backend/server.py +25 -13
- package/docs/architecture.md +280 -280
- package/docs/deployment.md +225 -225
- package/docs/runner.md +455 -455
- package/docs/skills.md +1 -1
- package/docs/workflow.md +157 -155
- package/package.json +1 -1
- package/runner/api_participant.mjs +235 -230
- package/runner/discussion_adapters.mjs +70 -2
- package/runner/free_runner.mjs +39 -3
- package/web/app.js +3382 -3384
- package/web/icons/typesafe.png +0 -0
- package/web/modern.css +1 -1
- package/web/modern.js +2174 -2042
package/runner/free_runner.mjs
CHANGED
|
@@ -9,6 +9,12 @@ import { ROUTE_CONFIDENCE, ROUTE_CONTEXT_MESSAGES, ROUTE_PROMPT_VERSION, ROUTE_W
|
|
|
9
9
|
|
|
10
10
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
11
11
|
const RENEW_MS = 30_000;
|
|
12
|
+
/** A changed phase is reported early, but no more often than this, since each report refreshes open pages. */
|
|
13
|
+
const ACTIVITY_REPORT_MS = 10_000;
|
|
14
|
+
/** Progress notes kept per execution, each cut to this length, and how many one renewal carries; the website enforces the same. */
|
|
15
|
+
const MAX_EXECUTION_NOTES = 50;
|
|
16
|
+
const MAX_NOTE_LENGTH = 500;
|
|
17
|
+
const MAX_NOTES_PER_RENEW = 10;
|
|
12
18
|
const MAX_DELIVERY_ATTEMPTS = 3;
|
|
13
19
|
const LIVE = ['reserving', 'delivering', 'sidecar_pending', 'running'];
|
|
14
20
|
const MAX_HASHED_BYTES = 5 * 1024 * 1024;
|
|
@@ -130,6 +136,9 @@ export class FreeRoomRunner {
|
|
|
130
136
|
this.members = new Map();
|
|
131
137
|
this.exited = new Set();
|
|
132
138
|
this.snapshots = new Map();
|
|
139
|
+
// Execution id -> { phase, at, sent_phase, sent_at, notes, next_seq }: what the member's software last showed it doing,
|
|
140
|
+
// and the progress notes not yet on the website.
|
|
141
|
+
this.activity = new Map();
|
|
133
142
|
this.context_errors = new Map();
|
|
134
143
|
this.adopt(config);
|
|
135
144
|
this.store.db.exec(`CREATE TABLE IF NOT EXISTS free_deliveries(member TEXT PRIMARY KEY, client_id TEXT NOT NULL,
|
|
@@ -329,7 +338,9 @@ export class FreeRoomRunner {
|
|
|
329
338
|
requests: execution.requests, read_after: execution.read_until },
|
|
330
339
|
on_binding: session => this.store.bind(member.name, session),
|
|
331
340
|
on_process: child => this.track(member.name, child),
|
|
332
|
-
on_output: (path, chunk) => appendFileSync(path, chunk, { mode: 0o600 }),
|
|
341
|
+
on_output: (path, chunk) => { appendFileSync(path, chunk, { mode: 0o600 }); this.touch(execution.id); },
|
|
342
|
+
on_activity: phase => this.touch(execution.id, phase),
|
|
343
|
+
on_note: text => this.note(execution.id, text),
|
|
333
344
|
// A stopped process may report after the next execution started; only its own execution takes the result.
|
|
334
345
|
on_failure: error => { try { if (this.row(member.name)?.execution_id === execution.id) this.save(member.name, { error }); } catch { /* Store may already be closed at shutdown. */ } },
|
|
335
346
|
on_exit: () => this.exited.add(execution.id),
|
|
@@ -357,6 +368,7 @@ export class FreeRoomRunner {
|
|
|
357
368
|
}
|
|
358
369
|
const execution = active.get(row.execution_id);
|
|
359
370
|
if (!execution) {
|
|
371
|
+
this.activity.delete(row.execution_id);
|
|
360
372
|
this.stop_member(member.name);
|
|
361
373
|
return this.save(member.name, { status: 'idle', error: '' });
|
|
362
374
|
}
|
|
@@ -369,11 +381,34 @@ export class FreeRoomRunner {
|
|
|
369
381
|
return this.save(member.name, { status: 'idle' });
|
|
370
382
|
}
|
|
371
383
|
if (Date.now() >= execution.created_at * 1000 + minutes * 60_000) return this.fail(member.name, execution.id, `超过单次执行时限 ${minutes} 分钟;请核对原会话确认是否已有内容`);
|
|
372
|
-
|
|
373
|
-
|
|
384
|
+
const activity = this.activity.get(execution.id);
|
|
385
|
+
const changed = activity && (activity.phase !== activity.sent_phase || activity.at !== activity.sent_at || activity.notes.length);
|
|
386
|
+
const elapsed = Date.now() - row.last_renew;
|
|
387
|
+
if (elapsed >= RENEW_MS || (changed && elapsed >= ACTIVITY_REPORT_MS)) {
|
|
388
|
+
const notes = activity?.notes.slice(0, MAX_NOTES_PER_RENEW) || [];
|
|
389
|
+
await api(this.config, `/executions/${execution.id}`, { action: 'renew',
|
|
390
|
+
...(activity ? { activity: activity.phase, activity_at: Math.floor(activity.at / 1000) } : {}), ...(notes.length ? { notes } : {}) });
|
|
391
|
+
if (activity) Object.assign(activity, { sent_phase: activity.phase, sent_at: activity.at, notes: activity.notes.slice(notes.length) });
|
|
374
392
|
this.save(member.name, { last_renew: Date.now() });
|
|
375
393
|
}
|
|
376
394
|
}
|
|
395
|
+
/** Note that the software produced output now, and its phase when the output names one. */
|
|
396
|
+
touch(execution_id, phase) {
|
|
397
|
+
// Updated in place: a renewal in flight marks what it sent on this same record.
|
|
398
|
+
if (!this.activity.has(execution_id)) this.activity.set(execution_id, { phase: '', notes: [], next_seq: 0 });
|
|
399
|
+
const current = this.activity.get(execution_id);
|
|
400
|
+
if (phase) current.phase = phase;
|
|
401
|
+
current.at = Date.now();
|
|
402
|
+
}
|
|
403
|
+
/** Queue one progress note for the next renewal; numbering makes a retried renewal harmless, and notes past the cap are dropped. */
|
|
404
|
+
note(execution_id, text) {
|
|
405
|
+
this.touch(execution_id);
|
|
406
|
+
const current = this.activity.get(execution_id);
|
|
407
|
+
if (current.next_seq >= MAX_EXECUTION_NOTES) return;
|
|
408
|
+
current.next_seq += 1;
|
|
409
|
+
// Cut by UTF-16 units like the website's check, without leaving half of a surrogate pair.
|
|
410
|
+
current.notes.push({ seq: current.next_seq, text: text.slice(0, MAX_NOTE_LENGTH).replace(/[\uD800-\uDBFF]$/, '') });
|
|
411
|
+
}
|
|
377
412
|
/** Report observed file changes once an execution has ended and its member's processes have exited. */
|
|
378
413
|
async report_finished(active) {
|
|
379
414
|
for (const [execution_id, snapshot] of this.snapshots) {
|
|
@@ -388,6 +423,7 @@ export class FreeRoomRunner {
|
|
|
388
423
|
}
|
|
389
424
|
/** Record the cause on the website, then stop this member's own processes. */
|
|
390
425
|
async fail(name, execution_id, reason) {
|
|
426
|
+
this.activity.delete(execution_id);
|
|
391
427
|
try { await api(this.config, `/executions/${execution_id}`, { action: 'fail', reason }); }
|
|
392
428
|
catch (error) { if (!/HTTP 409/.test(error.message)) throw error; }
|
|
393
429
|
this.save(name, { status: 'idle', error: reason });
|