polygram 0.38.2 → 0.38.3
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/approvals/store.js +45 -7
- package/lib/codex/runtime-controller.js +107 -1
- package/lib/db/event-detail-schema.js +523 -0
- package/lib/db/pairings.js +9 -1
- package/lib/db/secret-sweep.js +6 -3
- package/lib/db/sessions.js +2 -2
- package/lib/db.js +308 -49
- package/lib/handlers/abort.js +20 -3
- package/lib/handlers/approvals.js +2 -2
- package/lib/handlers/config-callback.js +7 -7
- package/lib/handlers/dispatcher.js +11 -9
- package/lib/handlers/questions.js +546 -45
- package/lib/handlers/slash-commands.js +54 -13
- package/lib/handlers/voice.js +1 -1
- package/lib/ops/clean-shutdown.js +5 -0
- package/lib/questions/questions.js +51 -0
- package/lib/questions/store.js +61 -5
- package/lib/rewind/execute.js +12 -2
- package/lib/rewind/rewind.js +1 -1
- package/lib/sdk/build-options.js +2 -2
- package/lib/sdk/callbacks.js +14 -9
- package/lib/secret-detect.js +280 -12
- package/lib/telegram/api.js +10 -2
- package/lib/telegram/rich-edit.js +9 -5
- package/lib/telegram/rich-send.js +6 -6
- package/migrations/020-drop-secret-fingerprint.sql +13 -0
- package/package.json +2 -2
- package/polygram.js +82 -45
package/polygram.js
CHANGED
|
@@ -121,7 +121,7 @@ const { createRedeliver } = require('./lib/handlers/redeliver');
|
|
|
121
121
|
const { classifyReplay, executeReplayPlan } = require('./lib/handlers/replay-disposition');
|
|
122
122
|
const { createDropRedeliverer } = require('./lib/handlers/drop-redeliver');
|
|
123
123
|
const { createSessionFeedback } = require('./lib/feedback/session-feedback');
|
|
124
|
-
const { createSlashCommands } = require('./lib/handlers/slash-commands');
|
|
124
|
+
const { createSlashCommands, normalizeCompactCommand } = require('./lib/handlers/slash-commands');
|
|
125
125
|
const {
|
|
126
126
|
buildCodexReconciliationView,
|
|
127
127
|
createHandleCodexReconciliationCallback,
|
|
@@ -1398,7 +1398,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1398
1398
|
// /pair-code /pairings /unpair /pair. Returns true when handled;
|
|
1399
1399
|
// caller short-circuits.
|
|
1400
1400
|
if (await dispatchSlashCommand({
|
|
1401
|
-
text, sessionKey, chatId, threadIdStr, chatConfig,
|
|
1401
|
+
text, msgId: msg.message_id, sessionKey, chatId, threadIdStr, chatConfig,
|
|
1402
1402
|
cmdUser, cmdUserId, label, sendReply,
|
|
1403
1403
|
})) return;
|
|
1404
1404
|
|
|
@@ -1511,7 +1511,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1511
1511
|
console.error(`[${label}] failed to notify user of skipped attachments: ${err.message}`);
|
|
1512
1512
|
logEvent('attachment-skip-notice-failed', {
|
|
1513
1513
|
chat_id: chatId, msg_id: msg.message_id,
|
|
1514
|
-
|
|
1514
|
+
error_class: err.name || 'Error',
|
|
1515
1515
|
rejected_count: rejected.length,
|
|
1516
1516
|
});
|
|
1517
1517
|
}
|
|
@@ -1793,7 +1793,8 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1793
1793
|
// Network errors can embed the request URL, which carries the
|
|
1794
1794
|
// bot token — redact before persisting, matching rich-edit.js
|
|
1795
1795
|
// and api.js's handling of the same error class.
|
|
1796
|
-
|
|
1796
|
+
api_error_code: err.code || err.name || 'error',
|
|
1797
|
+
error_len: err.message?.length ?? 0,
|
|
1797
1798
|
bot: BOT_NAME,
|
|
1798
1799
|
});
|
|
1799
1800
|
throw err;
|
|
@@ -2453,7 +2454,9 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2453
2454
|
logEvent('wedged-session-detected', {
|
|
2454
2455
|
chat_id: chatId, session_key: sessionKey,
|
|
2455
2456
|
kind: wedge.kind,
|
|
2456
|
-
|
|
2457
|
+
// The wrapper's classification is the forensic signal; the wrapped
|
|
2458
|
+
// payload is provider content and stays out of the row.
|
|
2459
|
+
text_len: result.text.length,
|
|
2457
2460
|
});
|
|
2458
2461
|
// Promote the wrapped error to result.error so the existing
|
|
2459
2462
|
// auto-recover + thrown-error machinery handles it uniformly.
|
|
@@ -2474,6 +2477,13 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2474
2477
|
// to type /new.
|
|
2475
2478
|
const cls = classifyError(result.error);
|
|
2476
2479
|
if (cls.autoRecover === 'reset_session') {
|
|
2480
|
+
// The process that owns any open question is going away with the
|
|
2481
|
+
// session. Retirement closes the row and drops what is held exactly
|
|
2482
|
+
// for it — that part runs synchronously, so starting it here is enough
|
|
2483
|
+
// to have happened before the reset. Only the card edit it leaves
|
|
2484
|
+
// behind is awaited, and the reset must not wait on Telegram.
|
|
2485
|
+
questionHandlers?.retireSession(sessionKey)
|
|
2486
|
+
?.catch((err) => console.error(`[${label}] question retire failed: ${err.message}`));
|
|
2477
2487
|
pm.resetSession(sessionKey, { reason: cls.kind })
|
|
2478
2488
|
.catch((err) => console.error(`[${label}] auto-reset failed: ${err.message}`));
|
|
2479
2489
|
logEvent('auto-recover', {
|
|
@@ -2648,7 +2658,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2648
2658
|
reactor.setState('ERROR');
|
|
2649
2659
|
logEvent('telegram-empty-response-fallback-failed', {
|
|
2650
2660
|
chat_id: chatId, msg_id: msg.message_id, bot: BOT_NAME,
|
|
2651
|
-
|
|
2661
|
+
error_class: err.name || 'Error',
|
|
2652
2662
|
});
|
|
2653
2663
|
throw new Error(`empty-response fallback send failed: ${err.message}`);
|
|
2654
2664
|
}
|
|
@@ -2737,7 +2747,8 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2737
2747
|
logEvent('canned-reply-suppressed', {
|
|
2738
2748
|
chat_id: chatId,
|
|
2739
2749
|
msg_id: msg.message_id,
|
|
2740
|
-
|
|
2750
|
+
// The suppressed reply is provider content; its size is the signal.
|
|
2751
|
+
original_len: sanitized.original?.length ?? 0,
|
|
2741
2752
|
backend: result?.backend || null,
|
|
2742
2753
|
});
|
|
2743
2754
|
parsed.text = sanitized.text;
|
|
@@ -3535,7 +3546,7 @@ function createBot(token) {
|
|
|
3535
3546
|
bot: BOT_NAME,
|
|
3536
3547
|
update_id: updateId,
|
|
3537
3548
|
msg_id: msgId,
|
|
3538
|
-
|
|
3549
|
+
error_class: err.name || 'Error',
|
|
3539
3550
|
});
|
|
3540
3551
|
});
|
|
3541
3552
|
|
|
@@ -3875,7 +3886,7 @@ async function main() {
|
|
|
3875
3886
|
}
|
|
3876
3887
|
} catch (err) {
|
|
3877
3888
|
console.error(`[secret-sweep] FAILED (${trigger}): ${err.message}`);
|
|
3878
|
-
try { db.logEvent('secret-sweep-failed', { trigger,
|
|
3889
|
+
try { db.logEvent('secret-sweep-failed', { trigger, error_class: err.name || 'Error' }); } catch {}
|
|
3879
3890
|
}
|
|
3880
3891
|
};
|
|
3881
3892
|
if (secretSweepCfg.enabled) {
|
|
@@ -4046,6 +4057,20 @@ async function main() {
|
|
|
4046
4057
|
answerQuestion: (sk, tc, result) => pm.answerQuestion(sk, tc, result),
|
|
4047
4058
|
logger: console,
|
|
4048
4059
|
});
|
|
4060
|
+
// A question whose answer was held in memory cannot be completed by this
|
|
4061
|
+
// process: the exact value went with the one that died. Cancel those rows
|
|
4062
|
+
// here — before replay, redelivery, provider recovery, polling or any other
|
|
4063
|
+
// path can act on them — so none is ever answered from a marker.
|
|
4064
|
+
// A failure here is fatal on purpose. Rows this could not cancel would meet
|
|
4065
|
+
// replay and inbound traffic with an answer no live process can complete, so
|
|
4066
|
+
// startup stops instead of admitting messages behind an open fence.
|
|
4067
|
+
try {
|
|
4068
|
+
await questionHandlers.reconcileMarkedQuestionsAtBoot(questionStore.listOpen(BOT_NAME));
|
|
4069
|
+
} catch (e) {
|
|
4070
|
+
console.error(`[${BOT_NAME}] question boot reconciliation failed; refusing to admit traffic: ${e.message}`);
|
|
4071
|
+
throw e;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4049
4074
|
// Resolve expired questions with {timedout} so claude never hangs on an ignored ask.
|
|
4050
4075
|
setInterval(() => {
|
|
4051
4076
|
try {
|
|
@@ -4059,7 +4084,10 @@ async function main() {
|
|
|
4059
4084
|
// the copy-only transcript-fork executor (P2/P3: fork → repoint the session → kill → delete
|
|
4060
4085
|
// orphaned bot messages). channels/cli only; the fork mechanism was proven in P0.6.
|
|
4061
4086
|
// See docs/0.13-rewind-design.md.
|
|
4062
|
-
const executeRewind = createRewindExecutor({
|
|
4087
|
+
const executeRewind = createRewindExecutor({
|
|
4088
|
+
db, pm, tg, bot, botName: BOT_NAME, logEvent, logger: console,
|
|
4089
|
+
retireQuestionSession: (sessionKey) => questionHandlers?.retireSession(sessionKey),
|
|
4090
|
+
});
|
|
4063
4091
|
rewindHandler = createRewindHandler({
|
|
4064
4092
|
pm, tg, bot, botName: BOT_NAME, logEvent, logger: console, executeRewind,
|
|
4065
4093
|
});
|
|
@@ -4407,6 +4435,7 @@ async function main() {
|
|
|
4407
4435
|
handleAbortIfRequested = createHandleAbort({
|
|
4408
4436
|
pm, bot, tg, logEvent, isAbortRequest,
|
|
4409
4437
|
markSessionAborted, clearAutosteeredReactions, getSessionKey,
|
|
4438
|
+
retireQuestionSession: (sessionKey) => questionHandlers?.retireSession(sessionKey),
|
|
4410
4439
|
botName: BOT_NAME, logger: console,
|
|
4411
4440
|
});
|
|
4412
4441
|
autosteer = createAutosteerHandlers({
|
|
@@ -4514,6 +4543,7 @@ async function main() {
|
|
|
4514
4543
|
modelVersionsDesc: MODEL_VERSIONS_DESC, saveConfig,
|
|
4515
4544
|
resolveRuntimeView: resolveSessionRuntimeView,
|
|
4516
4545
|
botName: BOT_NAME, logEvent, logger: console,
|
|
4546
|
+
retireQuestionSession: (sessionKey) => questionHandlers?.retireSession(sessionKey),
|
|
4517
4547
|
});
|
|
4518
4548
|
console.log('[polygram] using SDK ProcessManager');
|
|
4519
4549
|
|
|
@@ -4575,19 +4605,19 @@ async function main() {
|
|
|
4575
4605
|
// provider; ordinary stops keep the existing answer-and-await behavior.
|
|
4576
4606
|
try {
|
|
4577
4607
|
const openQuestions = questionStore.listOpen?.(BOT_NAME) || [];
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4608
|
+
const shutdownNotice = 'Bot is restarting — this question was cancelled. It may be re-asked in a moment.';
|
|
4609
|
+
// Each row is disposed on its own: one that throws must not take the
|
|
4610
|
+
// rows after it, nor the drop of everything held in memory, with it.
|
|
4611
|
+
for (const row of openQuestions) {
|
|
4612
|
+
try {
|
|
4613
|
+
if (continuationAuthorized) {
|
|
4614
|
+
questionHandlers.beginShutdownDisposition(row, { message: shutdownNotice });
|
|
4615
|
+
} else {
|
|
4616
|
+
// eslint-disable-next-line no-await-in-loop
|
|
4617
|
+
await questionHandlers.expireQuestion(row, { status: 'cancelled', message: shutdownNotice });
|
|
4618
|
+
}
|
|
4619
|
+
} catch (err) {
|
|
4620
|
+
console.error(`[shutdown] question ${row.id} disposition failed: ${err.message}`);
|
|
4591
4621
|
}
|
|
4592
4622
|
}
|
|
4593
4623
|
if (openQuestions.length) {
|
|
@@ -4595,6 +4625,11 @@ async function main() {
|
|
|
4595
4625
|
}
|
|
4596
4626
|
} catch (err) {
|
|
4597
4627
|
console.error(`[shutdown] question expiry failed: ${err.message}`);
|
|
4628
|
+
} finally {
|
|
4629
|
+
// Nothing exact survives the process on purpose, whatever any single row
|
|
4630
|
+
// did on the way out.
|
|
4631
|
+
try { questionHandlers.discardAll(); }
|
|
4632
|
+
catch (err) { console.error(`[shutdown] question context drop failed: ${err.message}`); }
|
|
4598
4633
|
}
|
|
4599
4634
|
|
|
4600
4635
|
// 1.6 Album siblings still buffered (arrived <flushMs before shutdown,
|
|
@@ -4687,6 +4722,7 @@ async function main() {
|
|
|
4687
4722
|
awaitIngressSettlement,
|
|
4688
4723
|
awaitHandlerSettlement,
|
|
4689
4724
|
settlementTimeoutMs: SHUTDOWN_DRAIN_MS,
|
|
4725
|
+
continuationAuthorized,
|
|
4690
4726
|
qualificationExpectation,
|
|
4691
4727
|
});
|
|
4692
4728
|
if (qualificationExpectation !== undefined) {
|
|
@@ -4799,7 +4835,7 @@ async function main() {
|
|
|
4799
4835
|
logEvent('shutdown-persistence-fallback', {
|
|
4800
4836
|
bot: BOT_NAME,
|
|
4801
4837
|
replay_marked: fallback.replayMarked,
|
|
4802
|
-
|
|
4838
|
+
error_class: err.name || 'Error',
|
|
4803
4839
|
});
|
|
4804
4840
|
} catch (fallbackErr) {
|
|
4805
4841
|
console.error(
|
|
@@ -5286,18 +5322,17 @@ async function main() {
|
|
|
5286
5322
|
console.error(`[replay] boot replay failed: ${err.message}`);
|
|
5287
5323
|
}
|
|
5288
5324
|
|
|
5289
|
-
//
|
|
5290
|
-
//
|
|
5291
|
-
//
|
|
5292
|
-
//
|
|
5293
|
-
//
|
|
5294
|
-
// freshly-spawned (resumed) Query." Same pattern as boot-replay
|
|
5295
|
-
// for inbound messages — recovery should be invisible.
|
|
5325
|
+
// Handle compact-command events that were never paired with a
|
|
5326
|
+
// compact-boundary (a deploy or crash interrupted them before the SDK
|
|
5327
|
+
// processed them). Recovery re-pushes the same /compact line into a
|
|
5328
|
+
// freshly-spawned (resumed) session, invisibly — the same pattern as
|
|
5329
|
+
// boot-replay for inbound messages.
|
|
5296
5330
|
//
|
|
5297
5331
|
// Requirements for a silent retry:
|
|
5298
|
-
// 1. The
|
|
5299
|
-
//
|
|
5300
|
-
//
|
|
5332
|
+
// 1. The command line is recoverable — from the inbound message row the
|
|
5333
|
+
// event points at, or from an older event that still stored it — and
|
|
5334
|
+
// it normalizes to a real /compact command. Otherwise the user gets
|
|
5335
|
+
// the "please retry" message instead.
|
|
5301
5336
|
// 2. The chat is still configured (config.chats[chat_id] exists).
|
|
5302
5337
|
// 3. The session has a saved claude_session_id we can resume.
|
|
5303
5338
|
//
|
|
@@ -5325,9 +5360,13 @@ async function main() {
|
|
|
5325
5360
|
const threadId = o.thread_id ? Number(o.thread_id) : null;
|
|
5326
5361
|
const savedSessionId = getClaudeSessionId(db, o.session_key);
|
|
5327
5362
|
|
|
5328
|
-
// Silent retry path: only when
|
|
5329
|
-
//
|
|
5330
|
-
|
|
5363
|
+
// Silent retry path: only when the stored line is provably a compact
|
|
5364
|
+
// command AND there is a session_id to resume into. The line comes back
|
|
5365
|
+
// from storage, so it is normalized and validated before it is fired at
|
|
5366
|
+
// a live session — a mention-suffixed command is accepted in its
|
|
5367
|
+
// normalized form, anything else is refused and surfaced instead.
|
|
5368
|
+
const replayCommand = normalizeCompactCommand(o.text);
|
|
5369
|
+
if (replayCommand && savedSessionId) {
|
|
5331
5370
|
try {
|
|
5332
5371
|
const entry = await pm.getOrSpawn(
|
|
5333
5372
|
o.session_key,
|
|
@@ -5339,7 +5378,7 @@ async function main() {
|
|
|
5339
5378
|
if (!entry || typeof entry.fireUserMessage !== 'function') {
|
|
5340
5379
|
throw new Error('Process.fireUserMessage not available');
|
|
5341
5380
|
}
|
|
5342
|
-
const ok = entry.fireUserMessage(
|
|
5381
|
+
const ok = entry.fireUserMessage(replayCommand);
|
|
5343
5382
|
if (!ok) {
|
|
5344
5383
|
throw new Error('fireUserMessage refused (closed or empty content)');
|
|
5345
5384
|
}
|
|
@@ -5348,8 +5387,7 @@ async function main() {
|
|
|
5348
5387
|
thread_id: o.thread_id,
|
|
5349
5388
|
session_key: o.session_key,
|
|
5350
5389
|
original_ts: o.ts,
|
|
5351
|
-
text_len:
|
|
5352
|
-
user: o.user,
|
|
5390
|
+
text_len: replayCommand.length,
|
|
5353
5391
|
user_id: o.user_id,
|
|
5354
5392
|
});
|
|
5355
5393
|
replayed += 1;
|
|
@@ -5360,9 +5398,9 @@ async function main() {
|
|
|
5360
5398
|
}
|
|
5361
5399
|
}
|
|
5362
5400
|
|
|
5363
|
-
// Fallback: surface the
|
|
5364
|
-
//
|
|
5365
|
-
// the silent
|
|
5401
|
+
// Fallback: surface the "please retry" message. Happens when the hint
|
|
5402
|
+
// could not be recovered, when the stored line is not a compact
|
|
5403
|
+
// command, or when the silent retry failed.
|
|
5366
5404
|
try {
|
|
5367
5405
|
await tg(bot, 'sendMessage', {
|
|
5368
5406
|
chat_id: o.chat_id,
|
|
@@ -5374,9 +5412,8 @@ async function main() {
|
|
|
5374
5412
|
thread_id: o.thread_id,
|
|
5375
5413
|
session_key: o.session_key,
|
|
5376
5414
|
original_ts: o.ts,
|
|
5377
|
-
user: o.user,
|
|
5378
5415
|
user_id: o.user_id,
|
|
5379
|
-
reason:
|
|
5416
|
+
reason: replayCommand ? 'spawn-failed' : (o.text ? 'not-a-compact-command' : 'no-recoverable-text'),
|
|
5380
5417
|
});
|
|
5381
5418
|
surfacedFallback += 1;
|
|
5382
5419
|
} catch (err) {
|