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.
@@ -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
- if (Date.now() - row.last_renew >= RENEW_MS) {
373
- await api(this.config, `/executions/${execution.id}`, { action: 'renew' });
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 });