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/.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 +8 -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
|
@@ -42,7 +42,8 @@ function createDispatcher({
|
|
|
42
42
|
botName,
|
|
43
43
|
logEvent,
|
|
44
44
|
// Closures that polygram.js owns; passed in:
|
|
45
|
-
handleMessage, // async (sessionKey, chatId, msg, bot)
|
|
45
|
+
handleMessage, // async (sessionKey, chatId, msg, bot, dispatchContext)
|
|
46
|
+
awaitSessionRecovery = () => Promise.resolve(),
|
|
46
47
|
sendToProcess, // async (sessionKey, prompt, ctx)
|
|
47
48
|
recoverCodex, // async ({ sessionKey, chatId, msg, bot, providerRecovery })
|
|
48
49
|
// Cross-cutting helpers:
|
|
@@ -265,7 +266,13 @@ function createDispatcher({
|
|
|
265
266
|
return resultText;
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
function dispatchHandleMessage(
|
|
269
|
+
function dispatchHandleMessage(
|
|
270
|
+
sessionKey,
|
|
271
|
+
chatId,
|
|
272
|
+
msg,
|
|
273
|
+
bot,
|
|
274
|
+
dispatchContext = null,
|
|
275
|
+
) {
|
|
269
276
|
const activeHandlerTarget = Object.freeze({
|
|
270
277
|
sessionKey: String(sessionKey),
|
|
271
278
|
chatId: String(chatId),
|
|
@@ -284,7 +291,19 @@ function createDispatcher({
|
|
|
284
291
|
in_flight: count, threshold: warnAt,
|
|
285
292
|
});
|
|
286
293
|
}
|
|
287
|
-
const task =
|
|
294
|
+
const task = Promise.resolve()
|
|
295
|
+
.then(() => awaitSessionRecovery(
|
|
296
|
+
sessionKey,
|
|
297
|
+
dispatchContext?.recoveryReceipt ?? null,
|
|
298
|
+
))
|
|
299
|
+
.then(() => handleMessage(
|
|
300
|
+
sessionKey,
|
|
301
|
+
chatId,
|
|
302
|
+
msg,
|
|
303
|
+
bot,
|
|
304
|
+
dispatchContext,
|
|
305
|
+
))
|
|
306
|
+
.catch(async (err) => {
|
|
288
307
|
const wasAborted = abortGrace.isRecent(sessionKey);
|
|
289
308
|
const isReplay = msg._isReplay === true;
|
|
290
309
|
let oomShutdown = false;
|
|
@@ -292,6 +311,10 @@ function createDispatcher({
|
|
|
292
311
|
oomShutdown = getOomObservation()?.detected === true;
|
|
293
312
|
} catch {}
|
|
294
313
|
const isShuttingDown = getIsShuttingDown() || oomShutdown;
|
|
314
|
+
const cleanReplayProcessDrift = (
|
|
315
|
+
dispatchContext?.cleanReplay != null
|
|
316
|
+
&& err.code === 'CODEX_CLEAN_REPLAY_PROCESS_CHANGED'
|
|
317
|
+
);
|
|
295
318
|
let processLossResetFailed = false;
|
|
296
319
|
logger.error?.(`[${sessionKey}] Error: ${err.message}`);
|
|
297
320
|
// Mark the row terminal so the right thing happens on next
|
|
@@ -306,12 +329,14 @@ function createDispatcher({
|
|
|
306
329
|
: isShuttingDown
|
|
307
330
|
? (isReplay ? 'replay-attempted' : 'replay-pending')
|
|
308
331
|
: 'failed';
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
332
|
+
if (!cleanReplayProcessDrift) {
|
|
333
|
+
dbWrite(() => (
|
|
334
|
+
db.setInboundHandlerStatusUnlessCodexTerminal
|
|
335
|
+
?? db.setInboundHandlerStatus
|
|
336
|
+
).call(db, {
|
|
337
|
+
chat_id: chatId, msg_id: msg.message_id, status,
|
|
338
|
+
}), `set handler_status=${status}`);
|
|
339
|
+
}
|
|
315
340
|
logEvent('handler-error', {
|
|
316
341
|
chat_id: chatId, session_key: sessionKey,
|
|
317
342
|
msg_id: msg?.message_id,
|
|
@@ -417,7 +442,13 @@ function createDispatcher({
|
|
|
417
442
|
// through into this hardcoded reply — contradicting the "chat is
|
|
418
443
|
// never told, only the operator is" contract classify.js establishes
|
|
419
444
|
// via userMessage: null (found in code review).
|
|
420
|
-
if (
|
|
445
|
+
if (
|
|
446
|
+
isReplay
|
|
447
|
+
&& !wasAborted
|
|
448
|
+
&& !isShuttingDown
|
|
449
|
+
&& err.code !== 'AUTH_DISABLED'
|
|
450
|
+
&& !cleanReplayProcessDrift
|
|
451
|
+
) {
|
|
421
452
|
tg(bot, 'sendMessage', {
|
|
422
453
|
chat_id: chatId,
|
|
423
454
|
text: '⚠️ This turn was interrupted and didn\'t complete on retry — please rephrase or simplify, or split into smaller steps.',
|
|
@@ -573,7 +604,7 @@ function createDispatcher({
|
|
|
573
604
|
});
|
|
574
605
|
}
|
|
575
606
|
}
|
|
576
|
-
|
|
607
|
+
}).finally(() => {
|
|
577
608
|
activeHandlerTargets.delete(activeHandlerTarget);
|
|
578
609
|
const n = (inFlightHandlers.get(sessionKey) || 1) - 1;
|
|
579
610
|
if (n <= 0) inFlightHandlers.delete(sessionKey);
|
|
@@ -63,10 +63,20 @@ function createRedeliver({
|
|
|
63
63
|
* @param {string|null} [opts.preMark] — handler_status to pre-mark (one-shot guard), e.g. 'replay-attempted'
|
|
64
64
|
* @param {boolean} [opts.gate=true] — run the D5 gate at tier 'redelivery'
|
|
65
65
|
* @param {boolean} [opts.ack=true] — 👀 on the redelivered message
|
|
66
|
+
* @param {Function|null} [opts.prepareDispatch] — post-gate durable setup
|
|
67
|
+
* returning private context for the normal dispatcher
|
|
68
|
+
* @param {Function|null} [opts.onGateBlocked] — terminalize a blocked replay
|
|
66
69
|
* @returns {Promise<{ok:boolean, reason?:string}>}
|
|
67
70
|
*/
|
|
68
71
|
return async function redeliverAsFreshTurn({
|
|
69
|
-
chatId,
|
|
72
|
+
chatId,
|
|
73
|
+
msg,
|
|
74
|
+
source,
|
|
75
|
+
preMark = null,
|
|
76
|
+
gate = true,
|
|
77
|
+
ack = true,
|
|
78
|
+
prepareDispatch = null,
|
|
79
|
+
onGateBlocked = null,
|
|
70
80
|
} = {}) {
|
|
71
81
|
if (!msg || !msg.chat || msg.message_id == null) return { ok: false, reason: 'malformed message' };
|
|
72
82
|
const chatConfig = config.chats[String(chatId)];
|
|
@@ -101,21 +111,42 @@ function createRedeliver({
|
|
|
101
111
|
logEvent('input-dropped-no-redeliver', {
|
|
102
112
|
chat_id: chatId, msg_id: msg.message_id, source, stage: res.stage ?? null,
|
|
103
113
|
});
|
|
104
|
-
|
|
114
|
+
if (typeof onGateBlocked === 'function') {
|
|
115
|
+
await onGateBlocked({
|
|
116
|
+
chatId,
|
|
117
|
+
msg,
|
|
118
|
+
source,
|
|
119
|
+
stage: res.stage ?? null,
|
|
120
|
+
action: res.action,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
reason: res.stage || res.action,
|
|
126
|
+
...(typeof onGateBlocked === 'function' ? { terminal: true } : {}),
|
|
127
|
+
};
|
|
105
128
|
}
|
|
106
129
|
}
|
|
107
130
|
|
|
131
|
+
const threadId = msg.message_thread_id?.toString();
|
|
132
|
+
const sessionKey = getSessionKey(String(chatId), threadId, chatConfig);
|
|
133
|
+
const dispatchContext = typeof prepareDispatch === 'function'
|
|
134
|
+
? await prepareDispatch({ chatId, msg, source, sessionKey })
|
|
135
|
+
: null;
|
|
108
136
|
if (ack) {
|
|
109
137
|
try { react?.(chatId, msg.message_id); } catch { /* best-effort — never blocks */ }
|
|
110
138
|
}
|
|
111
|
-
|
|
112
|
-
const threadId = msg.message_thread_id?.toString();
|
|
113
|
-
const sessionKey = getSessionKey(String(chatId), threadId, chatConfig);
|
|
114
139
|
logEvent('redelivered-as-fresh-turn', {
|
|
115
140
|
chat_id: chatId, msg_id: msg.message_id, source, session_key: sessionKey,
|
|
116
141
|
});
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
const task = dispatchHandleMessage(
|
|
143
|
+
sessionKey,
|
|
144
|
+
String(chatId),
|
|
145
|
+
msg,
|
|
146
|
+
bot,
|
|
147
|
+
dispatchContext,
|
|
148
|
+
);
|
|
149
|
+
return { ok: true, task };
|
|
119
150
|
};
|
|
120
151
|
}
|
|
121
152
|
|
|
@@ -53,14 +53,10 @@ function providerReplayDisposition(candidate, getProviderRecovery) {
|
|
|
53
53
|
* - CRASH (cleanShutdown=false): recover every still-unanswered candidate —
|
|
54
54
|
* the existing rc.57 behavior. An unexpected exit must never silently drop
|
|
55
55
|
* interrupted work.
|
|
56
|
-
* - CLEAN (cleanShutdown=true): a deliberate restart.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* INTENT, not message state: skip ALL pending candidates (don't re-answer
|
|
61
|
-
* stale messages) and surface ONE visibility notice per chat/topic so a
|
|
62
|
-
* genuinely-needed reply can be re-sent. rc.57's harm was *silent* loss;
|
|
63
|
-
* this makes the skip *visible*.
|
|
56
|
+
* - CLEAN (cleanShutdown=true): a deliberate restart. Exact Codex evidence
|
|
57
|
+
* that proves a request never crossed the provider boundary stays eligible
|
|
58
|
+
* for its dedicated recoverer. Other pending candidates are skipped, with
|
|
59
|
+
* one visibility notice per chat/topic for announceable legacy work.
|
|
64
60
|
*
|
|
65
61
|
* Dedup: a candidate already answered (hasCompletedTurn -> true) is dropped from
|
|
66
62
|
* the plan entirely — never recovered, never announced. The caller wires this to
|
|
@@ -89,6 +85,7 @@ function classifyReplay({
|
|
|
89
85
|
const pending = (candidates || []).filter((c) => !answered(c));
|
|
90
86
|
const legacy = [];
|
|
91
87
|
const recoverCodex = [];
|
|
88
|
+
const restartSkippedCodex = [];
|
|
92
89
|
const cancelled = [];
|
|
93
90
|
const defer = [];
|
|
94
91
|
|
|
@@ -100,7 +97,11 @@ function classifyReplay({
|
|
|
100
97
|
if (disposition.action === 'legacy') {
|
|
101
98
|
legacy.push(candidate);
|
|
102
99
|
} else if (disposition.action === 'recover') {
|
|
103
|
-
|
|
100
|
+
if (!cleanShutdown || disposition.cleanRestartSafe === true) {
|
|
101
|
+
recoverCodex.push(candidate);
|
|
102
|
+
} else {
|
|
103
|
+
restartSkippedCodex.push(candidate);
|
|
104
|
+
}
|
|
104
105
|
} else if (disposition.action === 'skip') {
|
|
105
106
|
cancelled.push(candidate);
|
|
106
107
|
} else {
|
|
@@ -121,15 +122,16 @@ function classifyReplay({
|
|
|
121
122
|
return plan;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
// Deliberate restart:
|
|
125
|
-
//
|
|
125
|
+
// Deliberate restart: preserve exact clean-safe Codex recovery, skip the
|
|
126
|
+
// remaining pending work, and group the ANNOUNCEABLE skipped rows per
|
|
127
|
+
// (chat, thread) for one visibility notice each
|
|
126
128
|
// (isolateTopics-safe). announceable excludes admin/slash + abort-shaped rows
|
|
127
129
|
// (H5) — those are skipped silently, mirroring the crash path's redelivery
|
|
128
130
|
// gate which never re-executes them. Default: everything announceable.
|
|
129
131
|
const isAnnounceable = typeof announceable === 'function' ? announceable : () => true;
|
|
130
132
|
const SEP = String.fromCharCode(0); // NUL separator (H8, collision-proof)
|
|
131
133
|
const groups = new Map();
|
|
132
|
-
const restartSkipped = [...legacy, ...
|
|
134
|
+
const restartSkipped = [...legacy, ...restartSkippedCodex];
|
|
133
135
|
for (const c of restartSkipped) {
|
|
134
136
|
if (!isAnnounceable(c)) continue;
|
|
135
137
|
const thread = c.thread_id == null ? null : c.thread_id;
|
|
@@ -143,6 +145,7 @@ function classifyReplay({
|
|
|
143
145
|
skip: [...restartSkipped, ...cancelled],
|
|
144
146
|
notices: Array.from(groups.values()),
|
|
145
147
|
};
|
|
148
|
+
if (recoverCodex.length > 0) plan.recoverCodex = recoverCodex;
|
|
146
149
|
if (defer.length > 0) plan.defer = defer;
|
|
147
150
|
return plan;
|
|
148
151
|
}
|
|
@@ -165,7 +168,7 @@ function classifyReplay({
|
|
|
165
168
|
* @param {object} opts.deps
|
|
166
169
|
* @param {(candidate)=>Promise<{ok:boolean}>} opts.deps.recover crash-path re-dispatch
|
|
167
170
|
* @param {(candidate)=>Promise<{ok:boolean}>} [opts.deps.recoverCodex]
|
|
168
|
-
* exact-runtime Codex
|
|
171
|
+
* exact-runtime Codex re-dispatch
|
|
169
172
|
* @param {(group)=>Promise<{ok:boolean, messageId?:any, error?:string}>} opts.deps.sendNotice
|
|
170
173
|
* @param {(candidate)=>void} opts.deps.markSkipped mark row terminal 'replay-skipped'
|
|
171
174
|
* @param {(kind, detail)=>void} [opts.deps.logEvent]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function scheduleCleanCodexReplaySessions({
|
|
4
|
+
candidates = [],
|
|
5
|
+
getSessionKey,
|
|
6
|
+
getContinuationTasks,
|
|
7
|
+
coordinator,
|
|
8
|
+
recover,
|
|
9
|
+
trackTask,
|
|
10
|
+
onOutcome = () => {},
|
|
11
|
+
} = {}) {
|
|
12
|
+
if (
|
|
13
|
+
!Array.isArray(candidates)
|
|
14
|
+
|| typeof getSessionKey !== 'function'
|
|
15
|
+
|| typeof getContinuationTasks !== 'function'
|
|
16
|
+
|| typeof coordinator?.schedule !== 'function'
|
|
17
|
+
|| typeof recover !== 'function'
|
|
18
|
+
|| typeof trackTask !== 'function'
|
|
19
|
+
|| typeof onOutcome !== 'function'
|
|
20
|
+
) {
|
|
21
|
+
throw new TypeError('clean Codex boot replay dependencies are invalid');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const candidatesBySession = new Map();
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
const sessionKey = getSessionKey(candidate);
|
|
27
|
+
if (typeof sessionKey !== 'string' || sessionKey.length === 0) {
|
|
28
|
+
throw new TypeError('clean Codex boot replay candidate has no session');
|
|
29
|
+
}
|
|
30
|
+
const sessionCandidates = candidatesBySession.get(sessionKey) || [];
|
|
31
|
+
sessionCandidates.push(candidate);
|
|
32
|
+
candidatesBySession.set(sessionKey, sessionCandidates);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let scheduled = 0;
|
|
36
|
+
for (const [sessionKey, followers] of candidatesBySession) {
|
|
37
|
+
const task = coordinator.schedule({
|
|
38
|
+
sessionKey,
|
|
39
|
+
continuationTasks: getContinuationTasks(sessionKey),
|
|
40
|
+
followers,
|
|
41
|
+
recover,
|
|
42
|
+
}).then((outcome) => {
|
|
43
|
+
onOutcome({ sessionKey, outcome });
|
|
44
|
+
return outcome;
|
|
45
|
+
});
|
|
46
|
+
scheduled += followers.length;
|
|
47
|
+
trackTask(task);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
scheduled,
|
|
52
|
+
sessions: candidatesBySession.size,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { scheduleCleanCodexReplaySessions };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createCleanReplaySessionCoordinator() {
|
|
4
|
+
const sessions = new Map();
|
|
5
|
+
|
|
6
|
+
function wait(sessionKey, receipt = null) {
|
|
7
|
+
const state = sessions.get(sessionKey);
|
|
8
|
+
if (!state || (receipt !== null && receipt === state.receipt)) {
|
|
9
|
+
return Promise.resolve();
|
|
10
|
+
}
|
|
11
|
+
return state.barrier;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function schedule({
|
|
15
|
+
sessionKey,
|
|
16
|
+
continuationTasks = [],
|
|
17
|
+
followers = [],
|
|
18
|
+
recover,
|
|
19
|
+
} = {}) {
|
|
20
|
+
if (
|
|
21
|
+
typeof sessionKey !== 'string'
|
|
22
|
+
|| sessionKey.length === 0
|
|
23
|
+
|| !Array.isArray(continuationTasks)
|
|
24
|
+
|| !Array.isArray(followers)
|
|
25
|
+
|| typeof recover !== 'function'
|
|
26
|
+
|| sessions.has(sessionKey)
|
|
27
|
+
) {
|
|
28
|
+
throw new TypeError('clean replay session schedule is invalid');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let release;
|
|
32
|
+
const state = {
|
|
33
|
+
receipt: null,
|
|
34
|
+
barrier: new Promise((resolve) => { release = resolve; }),
|
|
35
|
+
};
|
|
36
|
+
sessions.set(sessionKey, state);
|
|
37
|
+
|
|
38
|
+
const task = Promise.resolve().then(async () => {
|
|
39
|
+
let admitted = 0;
|
|
40
|
+
let terminal = 0;
|
|
41
|
+
try {
|
|
42
|
+
const continuationResults = await Promise.allSettled(continuationTasks);
|
|
43
|
+
if (continuationResults.some((result) => (
|
|
44
|
+
result.status !== 'fulfilled'
|
|
45
|
+
|| result.value?.status !== 'replied'
|
|
46
|
+
))) {
|
|
47
|
+
return {
|
|
48
|
+
status: 'deferred',
|
|
49
|
+
admitted,
|
|
50
|
+
terminal,
|
|
51
|
+
deferred: followers.length,
|
|
52
|
+
reason: 'continuation-not-replied',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (let index = 0; index < followers.length; index += 1) {
|
|
57
|
+
const receipt = Object.freeze({});
|
|
58
|
+
state.receipt = receipt;
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
// eslint-disable-next-line no-await-in-loop
|
|
62
|
+
result = await recover(followers[index], { receipt });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
result = {
|
|
65
|
+
status: 'failed',
|
|
66
|
+
reason: error?.code || 'recovery-failed',
|
|
67
|
+
};
|
|
68
|
+
} finally {
|
|
69
|
+
state.receipt = null;
|
|
70
|
+
}
|
|
71
|
+
if (result?.status === 'dispatched') {
|
|
72
|
+
admitted += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (result?.status === 'gate-terminal') {
|
|
76
|
+
terminal += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
status: 'deferred',
|
|
81
|
+
admitted,
|
|
82
|
+
terminal,
|
|
83
|
+
deferred: followers.length - index,
|
|
84
|
+
reason: result?.reason || 'recovery-not-dispatched',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
status: 'complete',
|
|
89
|
+
admitted,
|
|
90
|
+
terminal,
|
|
91
|
+
deferred: 0,
|
|
92
|
+
reason: null,
|
|
93
|
+
};
|
|
94
|
+
} finally {
|
|
95
|
+
if (sessions.get(sessionKey) === state) sessions.delete(sessionKey);
|
|
96
|
+
release();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return task;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Object.freeze({ wait, schedule });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { createCleanReplaySessionCoordinator };
|
package/lib/ops/clean-resume.js
CHANGED
package/lib/sdk/callbacks.js
CHANGED
|
@@ -97,19 +97,9 @@ function createSdkCallbacks({
|
|
|
97
97
|
const chatConfig = config.chats[entry.chatId] || {};
|
|
98
98
|
const topicConfig = getTopicConfig(chatConfig, entry.threadId || null);
|
|
99
99
|
if (isCodexProcess(entry)) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
provider: 'codex',
|
|
104
|
-
provider_session_id: event.providerSessionId ?? event.session_id,
|
|
105
|
-
app_server_session_id: event.appServerSessionId ?? null,
|
|
106
|
-
agent: null,
|
|
107
|
-
cwd: entry.cwd,
|
|
108
|
-
model: entry.model,
|
|
109
|
-
effort: entry.effort,
|
|
110
|
-
pm_backend: 'codex',
|
|
111
|
-
spawn_profile_id: entry.spawnProfileId,
|
|
112
|
-
}), `upsert Codex session ${sessionKey}`);
|
|
100
|
+
// Codex init does not carry the settings admitted for a turn. The
|
|
101
|
+
// request-prepared checkpoint persists that exact provider identity
|
|
102
|
+
// atomically with the turn attempt before any app-server write.
|
|
113
103
|
return;
|
|
114
104
|
}
|
|
115
105
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Boot replay only needs to distinguish zero, one, or conflicting evidence
|
|
2
|
+
-- for a Telegram input. These indexes keep those bounded probes independent
|
|
3
|
+
-- of the accumulated Codex attempt and linked-input history.
|
|
4
|
+
CREATE INDEX idx_codex_attempts_replay_source
|
|
5
|
+
ON codex_turn_attempts(
|
|
6
|
+
session_key,
|
|
7
|
+
telegram_source_message_id,
|
|
8
|
+
created_ts,
|
|
9
|
+
attempt_id
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX idx_codex_linked_inputs_replay_source
|
|
13
|
+
ON codex_linked_inputs(
|
|
14
|
+
telegram_chat_id,
|
|
15
|
+
telegram_message_id,
|
|
16
|
+
generation_id,
|
|
17
|
+
created_ts,
|
|
18
|
+
linked_input_id
|
|
19
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.5",
|
|
4
4
|
"description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
|
|
5
5
|
"main": "lib/ipc/client.js",
|
|
6
6
|
"bin": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@anthropic-ai/claude-agent-sdk": "0.2.123",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
|
-
"@shumkov/orchestra": "0.10.
|
|
66
|
+
"@shumkov/orchestra": "0.10.18",
|
|
67
67
|
"better-sqlite3": "^12.9.0",
|
|
68
68
|
"grammy": "^1.42.0",
|
|
69
69
|
"markdown-it": "^14.1.1",
|