polygram 0.38.3 → 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/.claude-plugin/plugin.json +1 -1
- package/lib/codex/clean-replay-dispatch.js +51 -0
- package/lib/codex/dispatch-reservation-id.js +20 -0
- package/lib/codex/runtime-controller.js +152 -18
- package/lib/codex/spawn-context.js +1 -0
- package/lib/db/auto-resume.js +161 -4
- package/lib/db/event-detail-schema.js +9 -0
- package/lib/db.js +642 -25
- package/lib/handlers/dispatcher.js +42 -11
- package/lib/handlers/redeliver.js +38 -7
- package/lib/handlers/replay-disposition.js +16 -13
- package/lib/ops/clean-codex-boot-replay.js +56 -0
- package/lib/ops/clean-replay-session-coordinator.js +106 -0
- package/lib/ops/clean-resume.js +4 -0
- package/lib/sdk/callbacks.js +3 -13
- package/migrations/021-codex-replay-evidence-indexes.sql +19 -0
- package/package.json +2 -2
- package/polygram.js +192 -24
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
|
|
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 (
|
|
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
|
-
}, {
|
|
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')
|
|
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
|
|
5155
|
-
|
|
5156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
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
|
|
5425
|
-
if (
|
|
5588
|
+
const recoveryTasks = cleanRecoveryTasksBySession.get(o.session_key);
|
|
5589
|
+
if (recoveryTasks?.length) {
|
|
5426
5590
|
deferred += 1;
|
|
5427
5591
|
trackHandlerTask(
|
|
5428
|
-
Promise.
|
|
5429
|
-
.then(() =>
|
|
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
|
}),
|