polygram 0.38.1 → 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 +353 -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 +24 -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/ipc/handlers.js +4 -0
- package/lib/ops/clean-restart-qualification.js +1 -0
- package/lib/ops/clean-shutdown.js +5 -0
- package/lib/ops/deploy-restart-handler.js +111 -0
- package/lib/ops/foreground-canary-target.js +454 -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 +117 -68
|
@@ -55,14 +55,21 @@ function createSlashCommands({
|
|
|
55
55
|
saveConfig = () => {},
|
|
56
56
|
botName,
|
|
57
57
|
logEvent,
|
|
58
|
+
// Narrow async hook: retire the question state of a session whose process
|
|
59
|
+
// this command is retiring. A callback, so this module never learns the
|
|
60
|
+
// question store.
|
|
61
|
+
retireQuestionSession = null,
|
|
58
62
|
logger = console,
|
|
59
63
|
} = {}) {
|
|
60
64
|
|
|
61
65
|
return async function dispatchSlashCommand(ctx) {
|
|
62
66
|
const {
|
|
63
|
-
text, sessionKey, chatId, threadIdStr, chatConfig,
|
|
67
|
+
text, msgId, sessionKey, chatId, threadIdStr, chatConfig,
|
|
64
68
|
cmdUser, cmdUserId, label, sendReply,
|
|
65
69
|
} = ctx;
|
|
70
|
+
// Commands are logged by their recognized verb, never by the typed line:
|
|
71
|
+
// the arguments are message content and telemetry stays content-free.
|
|
72
|
+
const commandVerb = () => String(text || '').trim().split(/\s+/)[0] || null;
|
|
66
73
|
const botAllowsCommands = !!config.bot?.allowConfigCommands;
|
|
67
74
|
|
|
68
75
|
// /context — route through pm.getContextUsage(sessionKey) so the
|
|
@@ -184,10 +191,11 @@ function createSlashCommands({
|
|
|
184
191
|
logEvent('compact-command', {
|
|
185
192
|
chat_id: chatId, thread_id: threadIdStr, session_key: sessionKey,
|
|
186
193
|
text_len: text.length,
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
// The id of the inbound message carrying the hint. Boot-time orphan
|
|
195
|
+
// recovery joins it back to the stored row to re-push silently; the
|
|
196
|
+
// hint itself is message content and never enters telemetry.
|
|
197
|
+
msg_id: msgId ?? null,
|
|
198
|
+
user_id: cmdUserId,
|
|
191
199
|
});
|
|
192
200
|
const hasHint = text.length > '/compact'.length + 1;
|
|
193
201
|
await sendReply(hasHint ? '🗜️ Compacting with your hint…' : '🗜️ Compacting…');
|
|
@@ -236,10 +244,18 @@ function createSlashCommands({
|
|
|
236
244
|
);
|
|
237
245
|
return true;
|
|
238
246
|
}
|
|
247
|
+
// The process that owned any open question is gone; retire its state.
|
|
248
|
+
// Ordered after the kill so a refused reload leaves the still-live
|
|
249
|
+
// interaction intact.
|
|
250
|
+
await retireQuestionSession?.(sessionKey);
|
|
251
|
+
} else {
|
|
252
|
+
// No process to retire, but an open question has nothing to deliver to
|
|
253
|
+
// either.
|
|
254
|
+
await retireQuestionSession?.(sessionKey);
|
|
239
255
|
}
|
|
240
256
|
logEvent('session-reload-command', {
|
|
241
|
-
chat_id: chatId, command:
|
|
242
|
-
|
|
257
|
+
chat_id: chatId, command: commandVerb(),
|
|
258
|
+
user_id: cmdUserId,
|
|
243
259
|
});
|
|
244
260
|
await sendReply(
|
|
245
261
|
codex
|
|
@@ -273,6 +289,10 @@ function createSlashCommands({
|
|
|
273
289
|
const codex = isCodexRuntimeView(runtimeView);
|
|
274
290
|
let drained = 0;
|
|
275
291
|
try {
|
|
292
|
+
// A fresh session retires the process that owns any open question.
|
|
293
|
+
// Deliberately before the reset: if the reset then fails, the safe
|
|
294
|
+
// outcome is still that nothing exact was kept.
|
|
295
|
+
await retireQuestionSession?.(sessionKey);
|
|
276
296
|
const r = await pm.resetSession(sessionKey, { reason: text.slice(1) });
|
|
277
297
|
drained = r?.drainedPendings ?? 0;
|
|
278
298
|
} catch (err) {
|
|
@@ -286,8 +306,8 @@ function createSlashCommands({
|
|
|
286
306
|
}
|
|
287
307
|
contextHintShown.delete(sessionKey);
|
|
288
308
|
logEvent('session-reset-command', {
|
|
289
|
-
chat_id: chatId, command:
|
|
290
|
-
|
|
309
|
+
chat_id: chatId, command: commandVerb(), drained_pendings: drained,
|
|
310
|
+
user_id: cmdUserId,
|
|
291
311
|
});
|
|
292
312
|
await sendReply(
|
|
293
313
|
codex
|
|
@@ -431,7 +451,7 @@ function createSlashCommands({
|
|
|
431
451
|
const auditRows = [{
|
|
432
452
|
chat_id: chatId, thread_id: wThread, field: 'model',
|
|
433
453
|
old_value: oldModel, new_value: newModel,
|
|
434
|
-
|
|
454
|
+
user_id: cmdUserId, source: 'command',
|
|
435
455
|
}];
|
|
436
456
|
if (effortAdjustment) {
|
|
437
457
|
auditRows.push({
|
|
@@ -594,7 +614,7 @@ function createSlashCommands({
|
|
|
594
614
|
const auditRow = {
|
|
595
615
|
chat_id: chatId, thread_id: wThread, field: 'effort',
|
|
596
616
|
old_value: oldEffort, new_value: newEffort,
|
|
597
|
-
|
|
617
|
+
user_id: cmdUserId, source: 'command',
|
|
598
618
|
};
|
|
599
619
|
if (codex) {
|
|
600
620
|
try {
|
|
@@ -689,7 +709,9 @@ function createSlashCommands({
|
|
|
689
709
|
});
|
|
690
710
|
logEvent('pair-code-issued', {
|
|
691
711
|
bot: botName, by: issuerId, scope: out.scope,
|
|
692
|
-
chat_id: out.chat_id,
|
|
712
|
+
chat_id: out.chat_id,
|
|
713
|
+
// The note is operator-typed text; record only that one was given.
|
|
714
|
+
noticed: Boolean(out.note),
|
|
693
715
|
});
|
|
694
716
|
const ttlLabel = args.ttl || '10m';
|
|
695
717
|
const chatLabel = out.chat_id ? `chat ${out.chat_id}` : 'the chat where it is redeemed';
|
|
@@ -765,4 +787,23 @@ function createSlashCommands({
|
|
|
765
787
|
};
|
|
766
788
|
}
|
|
767
789
|
|
|
768
|
-
|
|
790
|
+
/**
|
|
791
|
+
* Normalize a stored line into the exact `/compact [hint]` form, or return
|
|
792
|
+
* null when it is not a compact command at all.
|
|
793
|
+
*
|
|
794
|
+
* Boot-time recovery re-pushes a stored line into a live session, so the line
|
|
795
|
+
* has to be proven to BE a compact command first: an arbitrary message, or a
|
|
796
|
+
* different command, must never be fired at the agent as if the user had
|
|
797
|
+
* asked to compact. Telegram also delivers group commands with the bot's
|
|
798
|
+
* username attached (`/compact@bot hint`), which the agent's own parser does
|
|
799
|
+
* not accept — that suffix is stripped here.
|
|
800
|
+
*/
|
|
801
|
+
function normalizeCompactCommand(text) {
|
|
802
|
+
if (typeof text !== 'string') return null;
|
|
803
|
+
const match = /^\/compact(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
804
|
+
if (!match) return null;
|
|
805
|
+
const hint = (match[1] || '').trim();
|
|
806
|
+
return hint ? `/compact ${hint}` : '/compact';
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
module.exports = { createSlashCommands, normalizeCompactCommand };
|
package/lib/handlers/voice.js
CHANGED
|
@@ -75,7 +75,7 @@ function createTranscribeVoiceAttachments({
|
|
|
75
75
|
} catch (err) {
|
|
76
76
|
logger.error?.(`[${label}] transcribe failed for ${a.name}: ${err.message}`);
|
|
77
77
|
logEvent('voice-transcribe-failed', {
|
|
78
|
-
chat_id: chatId, msg_id: msgId,
|
|
78
|
+
chat_id: chatId, msg_id: msgId, error_class: err.name || 'Error',
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
}));
|
package/lib/ipc/handlers.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
* reload, so a value bound at wiring time can go stale.
|
|
23
23
|
* @param {(req) => any} opts.handleSendOverIpc
|
|
24
24
|
* @param {(req) => any} opts.requestDeployRestart
|
|
25
|
+
* @param {(req) => any} opts.requestForegroundCanaryTarget
|
|
25
26
|
* @param {() => any} opts.inspectCleanRestartQualification
|
|
26
27
|
*/
|
|
27
28
|
function createIpcHandlers({
|
|
@@ -30,6 +31,7 @@ function createIpcHandlers({
|
|
|
30
31
|
getInFlightHandlers,
|
|
31
32
|
handleSendOverIpc,
|
|
32
33
|
requestDeployRestart,
|
|
34
|
+
requestForegroundCanaryTarget,
|
|
33
35
|
inspectCleanRestartQualification,
|
|
34
36
|
} = {}) {
|
|
35
37
|
return {
|
|
@@ -46,6 +48,8 @@ function createIpcHandlers({
|
|
|
46
48
|
|
|
47
49
|
deploy_restart: (req) => requestDeployRestart(req),
|
|
48
50
|
|
|
51
|
+
foreground_canary_target: (req) => requestForegroundCanaryTarget(req),
|
|
52
|
+
|
|
49
53
|
clean_restart_qualification: async (req) => {
|
|
50
54
|
assertClosedQualificationRequest(req);
|
|
51
55
|
let observation = null;
|
|
@@ -130,6 +130,7 @@ function normalizeDeployQualificationExpectation(request) {
|
|
|
130
130
|
const requestKeys = Object.keys(request);
|
|
131
131
|
const allowedRequestKeys = new Set([
|
|
132
132
|
'op', 'id', 'secret', 'qualification_expectation',
|
|
133
|
+
'foreground_expectation',
|
|
133
134
|
]);
|
|
134
135
|
const hasUnknownRequestField = requestKeys.some(
|
|
135
136
|
(key) => !allowedRequestKeys.has(key),
|
|
@@ -6,6 +6,7 @@ async function prepareCleanRetirement({
|
|
|
6
6
|
awaitIngressSettlement,
|
|
7
7
|
awaitHandlerSettlement,
|
|
8
8
|
settlementTimeoutMs = 30_000,
|
|
9
|
+
continuationAuthorized = false,
|
|
9
10
|
qualificationExpectation,
|
|
10
11
|
} = {}) {
|
|
11
12
|
if (typeof deliveryBarrier?.fenceAndDrain !== 'function'
|
|
@@ -21,9 +22,13 @@ async function prepareCleanRetirement({
|
|
|
21
22
|
if (typeof awaitHandlerSettlement !== 'function') {
|
|
22
23
|
throw new TypeError('clean shutdown requires handler settlement');
|
|
23
24
|
}
|
|
25
|
+
if (typeof continuationAuthorized !== 'boolean') {
|
|
26
|
+
throw new TypeError('clean shutdown continuation authorization must be boolean');
|
|
27
|
+
}
|
|
24
28
|
|
|
25
29
|
const deliveryDrain = captureSettlement(() => deliveryBarrier.fenceAndDrain());
|
|
26
30
|
const processRetirement = captureSettlement(() => pm.retireForCleanRestart({
|
|
31
|
+
continuationAuthorized,
|
|
27
32
|
getDeliveryEvidence: (sessionKey, sourceMsgId) => (
|
|
28
33
|
deliveryBarrier.inspect(sessionKey, sourceMsgId)
|
|
29
34
|
),
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
requireRestartRequestId,
|
|
5
|
+
} = require('../ipc/restart-request-id');
|
|
6
|
+
const {
|
|
7
|
+
normalizeDeployQualificationExpectation,
|
|
8
|
+
} = require('./clean-restart-qualification');
|
|
9
|
+
const {
|
|
10
|
+
normalizeDeployForegroundExpectation,
|
|
11
|
+
} = require('./foreground-canary-target');
|
|
12
|
+
|
|
13
|
+
function createDeployRestartHandler({
|
|
14
|
+
getIsShuttingDown,
|
|
15
|
+
getPid,
|
|
16
|
+
foregroundCanaryAuthorizer,
|
|
17
|
+
persistForegroundCanaryAuthorization,
|
|
18
|
+
shutdown,
|
|
19
|
+
logger = console,
|
|
20
|
+
} = {}) {
|
|
21
|
+
if (typeof getIsShuttingDown !== 'function') {
|
|
22
|
+
throw new TypeError('deploy restart shutdown-state reader is required');
|
|
23
|
+
}
|
|
24
|
+
if (typeof getPid !== 'function') {
|
|
25
|
+
throw new TypeError('deploy restart PID reader is required');
|
|
26
|
+
}
|
|
27
|
+
if (typeof persistForegroundCanaryAuthorization !== 'function') {
|
|
28
|
+
throw new TypeError('foreground canary authorization persistence is required');
|
|
29
|
+
}
|
|
30
|
+
if (typeof shutdown !== 'function') {
|
|
31
|
+
throw new TypeError('deploy restart shutdown function is required');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rejected(oldPid, restartRequestId, rejectionCode) {
|
|
35
|
+
return {
|
|
36
|
+
accepted: false,
|
|
37
|
+
old_pid: oldPid,
|
|
38
|
+
restart_request_id: restartRequestId,
|
|
39
|
+
rejection_code: rejectionCode,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return function requestDeployRestart(request) {
|
|
44
|
+
const oldPid = getPid();
|
|
45
|
+
const isForegroundRequest = request != null
|
|
46
|
+
&& typeof request === 'object'
|
|
47
|
+
&& Object.hasOwn(request, 'foreground_expectation');
|
|
48
|
+
if (getIsShuttingDown() && !isForegroundRequest) {
|
|
49
|
+
const error = new Error('shutdown already in progress');
|
|
50
|
+
error.code = 'SHUTDOWN_IN_PROGRESS';
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
let restartRequestId;
|
|
54
|
+
try {
|
|
55
|
+
restartRequestId = requireRestartRequestId(request?.id);
|
|
56
|
+
} catch {
|
|
57
|
+
return rejected(oldPid, null, 'invalid-request');
|
|
58
|
+
}
|
|
59
|
+
if (getIsShuttingDown()) {
|
|
60
|
+
return rejected(oldPid, restartRequestId, 'shutdown-in-progress');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const qualificationExpectation = normalizeDeployQualificationExpectation(request);
|
|
64
|
+
const foregroundExpectation = normalizeDeployForegroundExpectation(request);
|
|
65
|
+
if (qualificationExpectation === null || foregroundExpectation === null) {
|
|
66
|
+
return rejected(oldPid, restartRequestId, 'invalid-request');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (foregroundExpectation !== undefined) {
|
|
70
|
+
if (typeof foregroundCanaryAuthorizer?.authorizeRestart !== 'function') {
|
|
71
|
+
return rejected(oldPid, restartRequestId, 'target-unavailable');
|
|
72
|
+
}
|
|
73
|
+
const authorization = foregroundCanaryAuthorizer.authorizeRestart({
|
|
74
|
+
requestId: restartRequestId,
|
|
75
|
+
expectation: foregroundExpectation,
|
|
76
|
+
});
|
|
77
|
+
if (!authorization?.accepted) {
|
|
78
|
+
return rejected(
|
|
79
|
+
oldPid,
|
|
80
|
+
restartRequestId,
|
|
81
|
+
authorization?.rejectionCode || 'target-unavailable',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
persistForegroundCanaryAuthorization(authorization.authorizationEvent);
|
|
86
|
+
} catch {
|
|
87
|
+
return rejected(
|
|
88
|
+
oldPid,
|
|
89
|
+
restartRequestId,
|
|
90
|
+
'authorization-evidence-unavailable',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
shutdown({
|
|
96
|
+
continuationAuthorized: true,
|
|
97
|
+
trigger: 'deploy-ipc',
|
|
98
|
+
restartRequestId,
|
|
99
|
+
qualificationExpectation,
|
|
100
|
+
}).catch((error) => {
|
|
101
|
+
logger.error?.(`[shutdown] deploy restart failed: ${error.message}`);
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
accepted: true,
|
|
105
|
+
old_pid: oldPid,
|
|
106
|
+
restart_request_id: restartRequestId,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { createDeployRestartHandler };
|