polygram 0.33.4 → 0.34.0
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/db/codex-reconciliation.js +4 -0
- package/lib/handlers/dispatcher.js +15 -16
- package/lib/ops/systemd-invocation.js +22 -0
- package/ops/README.md +13 -0
- package/package.json +1 -1
- package/polygram.js +15 -4
- package/scripts/ipc-smoke.js +28 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.34.0",
|
|
5
5
|
"description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
|
@@ -152,6 +152,10 @@ function listUnresolvedCodexAttempts(rawDb, input = {}) {
|
|
|
152
152
|
location.telegram_message_id,
|
|
153
153
|
unresolved.telegram_source_message_id
|
|
154
154
|
)
|
|
155
|
+
WHERE COALESCE(
|
|
156
|
+
location.telegram_message_id,
|
|
157
|
+
unresolved.telegram_source_message_id
|
|
158
|
+
) IS NOT NULL
|
|
155
159
|
ORDER BY unresolved.ambiguous_ts,
|
|
156
160
|
unresolved.attempt_id
|
|
157
161
|
`).all(sessionKey, sessionKey);
|
|
@@ -59,7 +59,7 @@ function createDispatcher({
|
|
|
59
59
|
// the historic 4096 for back-compat in synthetic test runs that pass
|
|
60
60
|
// pre-formatted text.
|
|
61
61
|
chunkBudget = 4096,
|
|
62
|
-
// Delay before a silent startup auto-retry re-dispatches
|
|
62
|
+
// Delay before a silent startup auto-retry re-dispatches a pre-delivery failure.
|
|
63
63
|
// Injected so tests can drive it to 0; production uses STARTUP_RETRY_DELAY_MS.
|
|
64
64
|
startupRetryDelayMs = STARTUP_RETRY_DELAY_MS,
|
|
65
65
|
// AUTH_DISABLED dedupe/re-arm gate (docs/AUTH_DISABLED_HANDLING_SPEC.md).
|
|
@@ -306,6 +306,11 @@ function createDispatcher({
|
|
|
306
306
|
chat_id: chatId, session_key: sessionKey,
|
|
307
307
|
msg_id: msg?.message_id,
|
|
308
308
|
error: err.message?.slice(0, 500),
|
|
309
|
+
code: err.code || undefined,
|
|
310
|
+
cause_code: err.cause?.code || undefined,
|
|
311
|
+
stderr_tail: err.code === 'TMUX_SPAWN_FAILED' && typeof err.stderr === 'string'
|
|
312
|
+
? err.stderr.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '').slice(-500)
|
|
313
|
+
: undefined,
|
|
309
314
|
stack: err.stack?.split('\n').slice(0, 5).join('\n'),
|
|
310
315
|
aborted: wasAborted || undefined,
|
|
311
316
|
replay: isReplay || undefined,
|
|
@@ -442,23 +447,17 @@ function createDispatcher({
|
|
|
442
447
|
}
|
|
443
448
|
}
|
|
444
449
|
const provider = providerRecovery?.provider;
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// the "🔄 reset it, resend" papercut, polygram just retries. One-shot
|
|
454
|
-
// (_startupRetried) so a host that genuinely can't start claude surfaces
|
|
455
|
-
// the friendly reset reply (below) after EXACTLY one retry, never a loop.
|
|
456
|
-
// Scoped to TMUX_SESSION_GONE only: CHANNELS_DIALOG_TIMEOUT is a real
|
|
457
|
-
// blocking dialog (usage-limit / permission) a retry would just re-hit,
|
|
458
|
-
// so it keeps its "please resend" copy.
|
|
459
|
-
if (err.code === 'TMUX_SESSION_GONE' && !msg._startupRetried) {
|
|
450
|
+
// Retry once when startup fails before the bridge can deliver the user's
|
|
451
|
+
// message. TMUX_SESSION_GONE is poison-cleared above; TMUX_SPAWN_FAILED
|
|
452
|
+
// is already torn down by CliProcess's start-failure cleanup. Mid-turn
|
|
453
|
+
// failures and blocking dialogs are excluded because replay could
|
|
454
|
+
// duplicate work or immediately re-hit the same user decision.
|
|
455
|
+
const startupRetryable = err.code === 'TMUX_SESSION_GONE'
|
|
456
|
+
|| err.code === 'TMUX_SPAWN_FAILED';
|
|
457
|
+
if (startupRetryable && !msg._startupRetried) {
|
|
460
458
|
logEvent('startup-auto-retry', {
|
|
461
459
|
chat_id: chatId, session_key: sessionKey, msg_id: msg?.message_id,
|
|
460
|
+
code: err.code,
|
|
462
461
|
});
|
|
463
462
|
// Re-dispatch a COPY carrying the one-shot marker — never mutate the
|
|
464
463
|
// caller's msg (the boot-replay path shares/re-reads it). unref the
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function parseSystemdInvocationId(env = process.env) {
|
|
4
|
+
const value = env.INVOCATION_ID;
|
|
5
|
+
if (value == null || value === '') return null;
|
|
6
|
+
if (!/^[0-9a-f]{32}$/.test(value)) {
|
|
7
|
+
throw new Error('invalid systemd invocation identity');
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function lifecycleDetail(detail, invocationId) {
|
|
13
|
+
const value = detail && typeof detail === 'object' ? detail : {};
|
|
14
|
+
return invocationId == null
|
|
15
|
+
? value
|
|
16
|
+
: { ...value, invocation_id: invocationId };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
parseSystemdInvocationId,
|
|
21
|
+
lifecycleDetail,
|
|
22
|
+
};
|
package/ops/README.md
CHANGED
|
@@ -109,6 +109,19 @@ canonical absolute directory. Temporary paths and unsafe aliases are rejected;
|
|
|
109
109
|
there is no `/tmp` fallback. If the data-directory path is too long for a
|
|
110
110
|
portable Unix socket, set `POLYGRAM_IPC_DIR` to a shorter owner-only path.
|
|
111
111
|
|
|
112
|
+
Deployment automation can query the aggregate in-flight handler count without
|
|
113
|
+
printing session identifiers or the socket path:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
POLYGRAM_IPC_DIR=/absolute/data-dir/.ipc polygram-ipc admin-bot busy
|
|
117
|
+
# {"bot":"admin-bot","in_flight":0}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This is an advisory quiet-window check. The daemon's shutdown admission,
|
|
121
|
+
delivery, and handler-settlement barriers remain the correctness boundary.
|
|
122
|
+
Invoking `polygram-ipc admin-bot` without a subcommand retains the existing
|
|
123
|
+
path-and-ping health output.
|
|
124
|
+
|
|
112
125
|
Allowed methods: `sendMessage`, `sendPhoto`, `sendDocument`, `sendSticker`,
|
|
113
126
|
`sendChatAction`, `editMessageText`, `setMessageReaction`. Other methods
|
|
114
127
|
are rejected server-side.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
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": {
|
package/polygram.js
CHANGED
|
@@ -32,6 +32,10 @@ const { buildPrompt, resolvePromptBackend } = require('./lib/prompt');
|
|
|
32
32
|
const { countInFlight } = require('./lib/queue-utils');
|
|
33
33
|
const { createIpcHandlers } = require('./lib/ipc/handlers');
|
|
34
34
|
const { classifyOrphanSweep } = require('./lib/ops/tmux-preflight');
|
|
35
|
+
const {
|
|
36
|
+
parseSystemdInvocationId,
|
|
37
|
+
lifecycleDetail,
|
|
38
|
+
} = require('./lib/ops/systemd-invocation');
|
|
35
39
|
const { filterAttachments, resolveFileCaps, resolveMaxFileOverride, MAX_TOTAL_BYTES } = require('./lib/attachments');
|
|
36
40
|
// 0.9.0: SDK ProcessManager is the only pm. CLI pm
|
|
37
41
|
// (lib/process-manager.js) deleted in commit 6.
|
|
@@ -3391,6 +3395,7 @@ let startPollWatchdog = null;
|
|
|
3391
3395
|
|
|
3392
3396
|
async function main() {
|
|
3393
3397
|
const cgroupOomObserver = createCgroupOomObserver();
|
|
3398
|
+
const invocationId = parseSystemdInvocationId();
|
|
3394
3399
|
let codexManagerOptions = Object.freeze({
|
|
3395
3400
|
codexHostIdentity: null,
|
|
3396
3401
|
codexBootSessionIdentity: null,
|
|
@@ -3628,7 +3633,10 @@ async function main() {
|
|
|
3628
3633
|
console.log(`[inbox] swept ${swept.swept} files (${(swept.bytes / 1_048_576).toFixed(1)} MiB) older than ${inboxRetentionMs / 86_400_000}d`);
|
|
3629
3634
|
db.logEvent('inbox-swept', { files: swept.swept, bytes: swept.bytes, retention_days: inboxRetentionMs / 86_400_000 });
|
|
3630
3635
|
}
|
|
3631
|
-
db.logEvent('polygram-start', {
|
|
3636
|
+
db.logEvent('polygram-start', lifecycleDetail({
|
|
3637
|
+
migration: migration.reason,
|
|
3638
|
+
imported: migration.imported,
|
|
3639
|
+
}, invocationId));
|
|
3632
3640
|
if (cgroupOomObserver.startup.status === 'unavailable') {
|
|
3633
3641
|
console.warn(`[oom-observer] unavailable (${cgroupOomObserver.startup.reason}); handled shutdown signals will retain clean-restart behavior`);
|
|
3634
3642
|
db.logEvent('oom-observer-unavailable', { reason: cgroupOomObserver.startup.reason });
|
|
@@ -4534,7 +4542,7 @@ async function main() {
|
|
|
4534
4542
|
});
|
|
4535
4543
|
}
|
|
4536
4544
|
}
|
|
4537
|
-
logEvent('shutdown-drain', {
|
|
4545
|
+
logEvent('shutdown-drain', lifecycleDetail({
|
|
4538
4546
|
bot: BOT_NAME,
|
|
4539
4547
|
in_flight: remaining,
|
|
4540
4548
|
in_flight_at_signal: inFlightAtSignal,
|
|
@@ -4543,7 +4551,7 @@ async function main() {
|
|
|
4543
4551
|
clean: res.clean,
|
|
4544
4552
|
shutdown_reason: res.shutdownReason,
|
|
4545
4553
|
...(res.oomKillDelta != null ? { oom_kill_delta: res.oomKillDelta } : {}),
|
|
4546
|
-
});
|
|
4554
|
+
}, invocationId));
|
|
4547
4555
|
console.log(`[shutdown] ${res.clean ? 'clean' : 'crash-like'} shutdown recorded (${res.shutdownReason}); ${inFlightAtSignal} in-flight at signal, drained ${drainElapsed}ms, ${remaining} still in-flight, ${res.replayMarked} marked replay-pending`);
|
|
4548
4556
|
} catch (err) {
|
|
4549
4557
|
console.error(`[shutdown] persistence failed: ${err.message}`);
|
|
@@ -4576,7 +4584,10 @@ async function main() {
|
|
|
4576
4584
|
if (cancelAllWaiters) cancelAllWaiters('cancelled', 'polygram shutting down');
|
|
4577
4585
|
if (pm && !pmRetired) await pm.shutdown().catch(() => {});
|
|
4578
4586
|
if (db) {
|
|
4579
|
-
try {
|
|
4587
|
+
try {
|
|
4588
|
+
db.logEvent('polygram-stop', lifecycleDetail({}, invocationId));
|
|
4589
|
+
db.raw.close();
|
|
4590
|
+
} catch {}
|
|
4580
4591
|
}
|
|
4581
4592
|
// rc.50: release our PID file claim so the next boot doesn't try
|
|
4582
4593
|
// to kill us. releasePidFile is idempotent and only deletes the
|
package/scripts/ipc-smoke.js
CHANGED
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Quick IPC round-trip probe.
|
|
4
|
-
* Usage: node scripts/ipc-smoke.js <bot-name>
|
|
4
|
+
* Usage: node scripts/ipc-smoke.js <bot-name> [busy]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const { call, socketPathFor } = require('../lib/ipc/client');
|
|
7
|
+
const { call, readSecret, socketPathFor } = require('../lib/ipc/client');
|
|
8
8
|
|
|
9
9
|
(async () => {
|
|
10
10
|
const bot = process.argv[2] || 'shumabit';
|
|
11
|
+
const command = process.argv[3] || 'ping';
|
|
11
12
|
const path = socketPathFor(bot);
|
|
12
13
|
|
|
14
|
+
if (command === 'busy') {
|
|
15
|
+
const result = await call({
|
|
16
|
+
path,
|
|
17
|
+
op: 'busy',
|
|
18
|
+
secret: readSecret(bot),
|
|
19
|
+
});
|
|
20
|
+
if (
|
|
21
|
+
result.ok !== true
|
|
22
|
+
|| result.bot !== bot
|
|
23
|
+
|| !Number.isSafeInteger(result.in_flight)
|
|
24
|
+
|| result.in_flight < 0
|
|
25
|
+
) {
|
|
26
|
+
throw new Error('invalid busy response');
|
|
27
|
+
}
|
|
28
|
+
console.log(JSON.stringify({
|
|
29
|
+
bot: result.bot,
|
|
30
|
+
in_flight: result.in_flight,
|
|
31
|
+
}));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
console.log('path:', path);
|
|
14
36
|
console.log('ping:', JSON.stringify(await call({ path, op: 'ping' })));
|
|
15
37
|
console.log('DONE');
|
|
16
38
|
})().catch((err) => {
|
|
17
|
-
|
|
39
|
+
const message = process.argv[3] === 'busy'
|
|
40
|
+
? 'busy request failed'
|
|
41
|
+
: err.message;
|
|
42
|
+
console.error('ERR:', message);
|
|
18
43
|
process.exit(1);
|
|
19
44
|
});
|