polygram 0.38.4 → 0.38.5

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/polygram.js CHANGED
@@ -85,6 +85,9 @@ const {
85
85
  createCodexRuntimeController,
86
86
  resolveCodexStartupRecovery,
87
87
  } = require('./lib/codex/runtime-controller');
88
+ const {
89
+ authorizeCleanReplayDispatch,
90
+ } = require('./lib/codex/clean-replay-dispatch');
88
91
  const {
89
92
  buildCodexSpawnContext,
90
93
  } = require('./lib/codex/spawn-context');
@@ -213,6 +216,12 @@ const {
213
216
  startCleanRestartRecovery,
214
217
  validateStrictResumeSpawn,
215
218
  } = require('./lib/ops/clean-resume');
219
+ const {
220
+ createCleanReplaySessionCoordinator,
221
+ } = require('./lib/ops/clean-replay-session-coordinator');
222
+ const {
223
+ scheduleCleanCodexReplaySessions,
224
+ } = require('./lib/ops/clean-codex-boot-replay');
216
225
  const { resolveReplayWindowMs } = require('./lib/db/replay-window');
217
226
  const { pruneEvents, resolveRetentionPolicy, validatePolicy } = require('./lib/db/events-retention');
218
227
  const { sweepSecrets, resolveSecretSweepConfig } = require('./lib/db/secret-sweep');
@@ -1222,7 +1231,7 @@ function parsePairCodeArgs(text) {
1222
1231
 
1223
1232
  // ─── Message handler ────────────────────────────────────────────────
1224
1233
 
1225
- async function handleMessage(sessionKey, chatId, msg, bot) {
1234
+ async function handleMessage(sessionKey, chatId, msg, bot, dispatchContext = null) {
1226
1235
  const chatConfig = config.chats[chatId];
1227
1236
  if (!chatConfig) return;
1228
1237
  const text = msg.text || msg.caption || '';
@@ -2056,24 +2065,40 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2056
2065
  let acceptedAutosteerProvider = null;
2057
2066
  try {
2058
2067
  const current = pm.get(sessionKey);
2059
- const liveCodexGeneration = Boolean(
2068
+ const cleanReplayDispatch = dispatchContext?.cleanReplay ?? null;
2069
+ const currentCodexGeneration = Boolean(
2060
2070
  current
2061
2071
  && !current.closed
2062
- && current.inFlight
2063
2072
  && typeof current.generationId === 'string'
2064
2073
  && (
2065
2074
  current.runtime === 'codex'
2066
2075
  || current.backend === 'codex'
2067
- ),
2076
+ )
2077
+ );
2078
+ const liveCodexGeneration = Boolean(
2079
+ currentCodexGeneration
2080
+ && current.inFlight
2068
2081
  );
2069
2082
  requireCodexDispatchEnabled({
2070
2083
  config,
2071
2084
  chatId,
2072
2085
  threadId: threadIdStr || null,
2073
2086
  selectedProvider: selectedInboundProvider,
2074
- liveCodexGeneration,
2087
+ liveCodexGeneration: cleanReplayDispatch
2088
+ ? currentCodexGeneration
2089
+ : liveCodexGeneration,
2075
2090
  });
2076
- if (liveCodexGeneration) {
2091
+ if (cleanReplayDispatch) {
2092
+ codexDispatch = authorizeCleanReplayDispatch({
2093
+ sessionKey,
2094
+ currentProcess: current,
2095
+ expectedProcess: cleanReplayDispatch.expectedProcess,
2096
+ reservation: cleanReplayDispatch.reservation,
2097
+ controller: codexRuntimeController,
2098
+ });
2099
+ codexDispatchDecision = 'queue';
2100
+ acceptedAutosteerProvider = 'codex';
2101
+ } else if (liveCodexGeneration) {
2077
2102
  if (!codexRuntimeController) {
2078
2103
  codexDispatchDecision = 'unavailable';
2079
2104
  } else {
@@ -2284,7 +2309,13 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2284
2309
  // emitting — not the moment we wrote stdin.
2285
2310
  onFirstStream: () => reactor.setState('THINKING'),
2286
2311
  onTurnId: (id) => { dispatchedTurnId = id; },
2287
- }, { onDispatched: dispatched })
2312
+ }, {
2313
+ expectedProcess: cleanReplayDispatch?.expectedProcess ?? null,
2314
+ onDispatched: () => {
2315
+ dispatched();
2316
+ try { dispatchContext?.onDispatched?.(); } catch {}
2317
+ },
2318
+ })
2288
2319
  .catch((e) => ({ __sendError: e }))
2289
2320
  .finally(dispatched);
2290
2321
  });
@@ -3571,6 +3602,8 @@ async function main() {
3571
3602
  mainPath: __filename,
3572
3603
  packageVersion: packageMetadata.version,
3573
3604
  });
3605
+ const cleanReplaySessionCoordinator =
3606
+ createCleanReplaySessionCoordinator();
3574
3607
  const cgroupOomObserver = createCgroupOomObserver();
3575
3608
  const invocationId = parseSystemdInvocationId();
3576
3609
  let abnormalProviderTermination = null;
@@ -4344,7 +4377,12 @@ async function main() {
4344
4377
  });
4345
4378
  const processFactory = (sessionKey, spawnContext) => {
4346
4379
  const proc = orchestraProcessFactory(sessionKey, spawnContext);
4347
- if (proc?.runtime === 'codex') codexRuntimeController.registerProcess(proc);
4380
+ if (proc?.runtime === 'codex') {
4381
+ codexRuntimeController.registerProcess(proc, {
4382
+ expectedProviderGenerationId:
4383
+ spawnContext.expectedProviderGenerationId ?? null,
4384
+ });
4385
+ }
4348
4386
  return proc;
4349
4387
  };
4350
4388
  // Route in-process approval prompts through the SAME canUseTool plumbing
@@ -4463,6 +4501,9 @@ async function main() {
4463
4501
  } = createDispatcher({
4464
4502
  config, db, dbWrite, tg, botName: BOT_NAME, logEvent,
4465
4503
  handleMessage, sendToProcess,
4504
+ awaitSessionRecovery: (sessionKey, receipt) => (
4505
+ cleanReplaySessionCoordinator.wait(sessionKey, receipt)
4506
+ ),
4466
4507
  recoverCodex: (input) => {
4467
4508
  if (typeof recoverCodexRequest !== 'function') {
4468
4509
  const error = new Error('Exact Codex recovery path is unavailable');
@@ -5151,11 +5192,9 @@ async function main() {
5151
5192
  for (let index = 0; index < cleanRecovery.claims.length; index += 1) {
5152
5193
  const sessionKey = cleanRecovery.claims[index].session_key;
5153
5194
  const task = cleanRecovery.tasks[index];
5154
- const previous = cleanRecoveryTasksBySession.get(sessionKey);
5155
- cleanRecoveryTasksBySession.set(
5156
- sessionKey,
5157
- previous ? Promise.all([previous, task]) : task,
5158
- );
5195
+ const tasks = cleanRecoveryTasksBySession.get(sessionKey) || [];
5196
+ tasks.push(task);
5197
+ cleanRecoveryTasksBySession.set(sessionKey, tasks);
5159
5198
  }
5160
5199
  } catch (err) {
5161
5200
  const fatal = new Error(
@@ -5215,20 +5254,26 @@ async function main() {
5215
5254
  return msg;
5216
5255
  };
5217
5256
 
5257
+ const providerRecoveries = new Map();
5258
+ const recoveryKey = (row) => `${row.chat_id}\u0000${row.msg_id}`;
5218
5259
  const getProviderRecovery = (row) => {
5219
5260
  const sessionKey = sourceSessionKey(row);
5220
5261
  if (!sessionKey) {
5221
- return {
5262
+ const missing = {
5222
5263
  provider: 'unknown',
5223
5264
  reason: 'session-configuration-missing',
5224
5265
  };
5266
+ providerRecoveries.set(recoveryKey(row), missing);
5267
+ return missing;
5225
5268
  }
5226
- return db.getReplayProviderRecovery({
5269
+ const evidence = db.getReplayProviderRecovery({
5227
5270
  sessionKey,
5228
5271
  botName: BOT_NAME,
5229
5272
  telegramChatId: String(row.chat_id),
5230
5273
  telegramMessageId: String(row.msg_id),
5231
5274
  });
5275
+ providerRecoveries.set(recoveryKey(row), evidence);
5276
+ return evidence;
5232
5277
  };
5233
5278
 
5234
5279
  const plan = classifyReplay({
@@ -5239,8 +5284,124 @@ async function main() {
5239
5284
  getProviderRecovery,
5240
5285
  });
5241
5286
 
5287
+ let cleanCodexScheduled = 0;
5288
+ let executionPlan = plan;
5289
+ if (cleanShutdown && (plan.recoverCodex?.length || 0) > 0) {
5290
+ const cleanReplaySchedule = scheduleCleanCodexReplaySessions({
5291
+ candidates: plan.recoverCodex,
5292
+ getSessionKey: sourceSessionKey,
5293
+ getContinuationTasks: (sessionKey) => (
5294
+ cleanRecoveryTasksBySession.get(sessionKey) || []
5295
+ ),
5296
+ coordinator: cleanReplaySessionCoordinator,
5297
+ recover: async (row, { receipt: recoveryReceipt }) => {
5298
+ const msg = reconstruct(row);
5299
+ msg._requiredProvider = 'codex';
5300
+ let resolveAdmission;
5301
+ const admitted = new Promise((resolve) => {
5302
+ resolveAdmission = resolve;
5303
+ });
5304
+ const redelivery = await redeliverAsFreshTurn({
5305
+ chatId: row.chat_id,
5306
+ msg,
5307
+ source: 'boot-replay-codex-clean',
5308
+ preMark: null,
5309
+ onGateBlocked: () => db.setInboundHandlerStatus({
5310
+ chat_id: row.chat_id,
5311
+ msg_id: row.msg_id,
5312
+ status: 'replay-skipped',
5313
+ }),
5314
+ prepareDispatch: async () => {
5315
+ const expectedProcess = await pm.getOrSpawn(
5316
+ sessionKey,
5317
+ await buildSpawnContext(sessionKey),
5318
+ );
5319
+ if (
5320
+ !expectedProcess
5321
+ || expectedProcess.closed
5322
+ || pm.get(sessionKey) !== expectedProcess
5323
+ || (
5324
+ expectedProcess.runtime !== 'codex'
5325
+ && expectedProcess.backend !== 'codex'
5326
+ )
5327
+ ) {
5328
+ const error = new Error(
5329
+ 'Clean Codex replay lost its exact process before rearm',
5330
+ );
5331
+ error.code = 'CODEX_CLEAN_REPLAY_PROCESS_CHANGED';
5332
+ throw error;
5333
+ }
5334
+ const replayReceipt = db.prepareCodexCleanReplay({
5335
+ source: {
5336
+ botName: BOT_NAME,
5337
+ telegramChatId: String(row.chat_id),
5338
+ telegramMessageId: String(row.msg_id),
5339
+ },
5340
+ sessionKey,
5341
+ currentGeneration: {
5342
+ generationId: expectedProcess.generationId,
5343
+ },
5344
+ expectedEvidence:
5345
+ providerRecoveries.get(recoveryKey(row)),
5346
+ owner: {
5347
+ stableHostId: expectedProcess.hostIdentity,
5348
+ bootSessionId: expectedProcess.bootSessionIdentity,
5349
+ },
5350
+ ts: Date.now(),
5351
+ });
5352
+ return Object.freeze({
5353
+ recoveryReceipt,
5354
+ cleanReplay: Object.freeze({
5355
+ expectedProcess,
5356
+ reservation: Object.freeze({
5357
+ reservationId: replayReceipt.reservationId,
5358
+ generationId: replayReceipt.generationId,
5359
+ }),
5360
+ }),
5361
+ onDispatched: () => resolveAdmission({
5362
+ status: 'dispatched',
5363
+ }),
5364
+ });
5365
+ },
5366
+ });
5367
+ if (!redelivery.ok) {
5368
+ return redelivery.terminal
5369
+ ? { status: 'gate-terminal' }
5370
+ : {
5371
+ status: 'failed',
5372
+ reason: redelivery.reason || 'redelivery-refused',
5373
+ };
5374
+ }
5375
+ const handlerSettled = Promise.resolve(redelivery.task).then(
5376
+ () => ({
5377
+ status: 'failed',
5378
+ reason: 'handler-settled-before-admission',
5379
+ }),
5380
+ (error) => ({
5381
+ status: 'failed',
5382
+ reason: error?.code || 'handler-failed-before-admission',
5383
+ }),
5384
+ );
5385
+ return Promise.race([admitted, handlerSettled]);
5386
+ },
5387
+ trackTask: trackHandlerTask,
5388
+ onOutcome: ({ sessionKey, outcome }) => {
5389
+ logEvent('codex-clean-replay-session', {
5390
+ session_key: sessionKey,
5391
+ status: outcome.status,
5392
+ admitted_count: outcome.admitted,
5393
+ terminal_count: outcome.terminal,
5394
+ deferred_count: outcome.deferred,
5395
+ reason: outcome.reason || undefined,
5396
+ });
5397
+ },
5398
+ });
5399
+ cleanCodexScheduled = cleanReplaySchedule.scheduled;
5400
+ executionPlan = { ...plan, recoverCodex: [] };
5401
+ }
5402
+
5242
5403
  const result = await executeReplayPlan({
5243
- plan,
5404
+ plan: executionPlan,
5244
5405
  deps: {
5245
5406
  // CRASH path — unchanged: through the unified redelivery tail (D5 gate
5246
5407
  // at tier 'redelivery', 'replay-attempted' one-shot pre-mark, ack).
@@ -5307,12 +5468,15 @@ async function main() {
5307
5468
  });
5308
5469
 
5309
5470
  if (candidates.length > 0) {
5310
- console.log(`[replay] ${cleanShutdown ? 'clean restart' : 'crash'} — recovered ${result.recovered}, skipped ${result.skipped}, noticed ${result.noticed}${result.noticeFailed ? `, notice-failed ${result.noticeFailed}` : ''}${result.deferred ? `, deferred ${result.deferred}` : ''}`);
5471
+ console.log(`[replay] ${cleanShutdown ? 'clean restart' : 'crash'} — recovered ${result.recovered}, scheduled ${cleanCodexScheduled}, skipped ${result.skipped}, noticed ${result.noticed}${result.noticeFailed ? `, notice-failed ${result.noticeFailed}` : ''}${result.deferred ? `, deferred ${result.deferred}` : ''}`);
5311
5472
  logEvent('replay-on-boot', {
5312
5473
  bot: BOT_NAME, clean: cleanShutdown,
5313
- recovered: result.recovered, skipped: result.skipped,
5314
- noticed: result.noticed, notice_failed: result.noticeFailed,
5315
- deferred: result.deferred || 0,
5474
+ recovered_count: result.recovered,
5475
+ skipped_count: result.skipped,
5476
+ codex_scheduled_count: cleanCodexScheduled,
5477
+ noticed_count: result.noticed,
5478
+ notice_failed_count: result.noticeFailed,
5479
+ deferred_count: result.deferred || 0,
5316
5480
  total: candidates.length,
5317
5481
  });
5318
5482
  }
@@ -5421,12 +5585,16 @@ async function main() {
5421
5585
  }
5422
5586
  };
5423
5587
  for (const o of orphansLatest.values()) {
5424
- const recoveryTask = cleanRecoveryTasksBySession.get(o.session_key);
5425
- if (recoveryTask) {
5588
+ const recoveryTasks = cleanRecoveryTasksBySession.get(o.session_key);
5589
+ if (recoveryTasks?.length) {
5426
5590
  deferred += 1;
5427
5591
  trackHandlerTask(
5428
- Promise.resolve(recoveryTask)
5429
- .then(() => recoverCompact(o))
5592
+ Promise.all(recoveryTasks)
5593
+ .then((results) => (
5594
+ results.every((result) => result?.status === 'replied')
5595
+ ? recoverCompact(o)
5596
+ : undefined
5597
+ ))
5430
5598
  .catch((err) => {
5431
5599
  console.error(`[compact-replay] ${o.session_key}: deferred recovery failed: ${err.message}`);
5432
5600
  }),