polygram 0.21.0 → 0.23.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/README.md +5 -3
- package/config.example.json +23 -1
- package/lib/codex/binary.js +287 -0
- package/lib/codex/delivery-finalizer.js +97 -0
- package/lib/codex/diagnostics.js +483 -0
- package/lib/codex/event-normalizer.js +50 -0
- package/lib/codex/host-identity.js +103 -0
- package/lib/codex/model-catalog.js +359 -0
- package/lib/codex/retention-scheduler.js +59 -0
- package/lib/codex/runtime-controller.js +1292 -0
- package/lib/codex/runtime-profile.js +888 -0
- package/lib/codex/spawn-context.js +87 -0
- package/lib/db/auto-resume.js +77 -9
- package/lib/db/codex-reconciliation.js +490 -0
- package/lib/db/codex-retention.js +139 -0
- package/lib/db/sessions.js +102 -1
- package/lib/db.js +3418 -19
- package/lib/error/classify.js +140 -0
- package/lib/handlers/abort.js +143 -5
- package/lib/handlers/autosteer.js +256 -1
- package/lib/handlers/codex-reconciliation.js +230 -0
- package/lib/handlers/config-callback.js +324 -89
- package/lib/handlers/config-ui.js +401 -39
- package/lib/handlers/dispatcher.js +71 -8
- package/lib/handlers/edit-correction.js +11 -1
- package/lib/handlers/edit-redelivery.js +230 -4
- package/lib/handlers/gate-inbound.js +7 -0
- package/lib/handlers/replay-disposition.js +133 -11
- package/lib/handlers/slash-commands.js +443 -37
- package/lib/ipc/client.js +22 -12
- package/lib/ipc/runtime-directory.js +208 -0
- package/lib/ipc/server.js +18 -12
- package/lib/model-costs.js +4 -3
- package/lib/pm-interface.js +33 -0
- package/lib/prompt.js +15 -9
- package/lib/rewind/rewind.js +21 -1
- package/lib/runtime-config.js +605 -0
- package/lib/sdk/build-options.js +4 -1
- package/lib/sdk/callbacks.js +46 -3
- package/lib/util/claude-session-jsonl.js +3 -2
- package/migrations/015-agent-runtime-sessions.sql +50 -0
- package/migrations/016-agent-turn-attempts.sql +211 -0
- package/migrations/017-config-runtime-changes.sql +46 -0
- package/ops/README.md +57 -0
- package/package.json +2 -2
- package/polygram.js +697 -48
- package/scripts/doctor.js +37 -8
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DIRECTORY_NAME = '.ipc';
|
|
8
|
+
const BOT_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
9
|
+
const MAX_BOT_NAME_BYTES = 64;
|
|
10
|
+
const MAX_UNIX_SOCKET_PATH_BYTES = 103;
|
|
11
|
+
|
|
12
|
+
function ipcDirectoryError(code, message) {
|
|
13
|
+
const error = new Error(message);
|
|
14
|
+
error.code = code;
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function currentUid() {
|
|
19
|
+
if (typeof process.getuid !== 'function') {
|
|
20
|
+
throw ipcDirectoryError(
|
|
21
|
+
'IPC_DIR_UID_UNAVAILABLE',
|
|
22
|
+
'IPC directory ownership cannot be verified on this platform',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return process.getuid();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isWithin(candidate, root) {
|
|
29
|
+
const relative = path.relative(root, candidate);
|
|
30
|
+
return (
|
|
31
|
+
relative === ''
|
|
32
|
+
|| (
|
|
33
|
+
!relative.startsWith(`..${path.sep}`)
|
|
34
|
+
&& relative !== '..'
|
|
35
|
+
&& !path.isAbsolute(relative)
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function existingRealpath(candidate) {
|
|
41
|
+
try {
|
|
42
|
+
return fs.realpathSync.native(candidate);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function temporaryRoots() {
|
|
49
|
+
const roots = new Set([
|
|
50
|
+
'/tmp',
|
|
51
|
+
'/private/tmp',
|
|
52
|
+
'/var/tmp',
|
|
53
|
+
'/private/var/tmp',
|
|
54
|
+
path.resolve(os.tmpdir()),
|
|
55
|
+
]);
|
|
56
|
+
for (const root of [...roots]) {
|
|
57
|
+
const canonical = existingRealpath(root);
|
|
58
|
+
if (canonical) roots.add(canonical);
|
|
59
|
+
}
|
|
60
|
+
return [...roots];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertNonTemporary(candidate) {
|
|
64
|
+
const paths = [candidate];
|
|
65
|
+
const canonical = existingRealpath(candidate);
|
|
66
|
+
if (canonical) paths.push(canonical);
|
|
67
|
+
if (paths.some((item) => temporaryRoots().some((root) => isWithin(item, root)))) {
|
|
68
|
+
throw ipcDirectoryError(
|
|
69
|
+
'IPC_DIR_TEMPORARY',
|
|
70
|
+
'IPC directory must not be located in a temporary directory',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertOwnedDirectory(candidate, { exactMode = null, label = 'IPC directory' } = {}) {
|
|
76
|
+
let entry;
|
|
77
|
+
try {
|
|
78
|
+
entry = fs.lstatSync(candidate);
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
throw ipcDirectoryError('IPC_DIR_UNAVAILABLE', `${label} is unavailable`);
|
|
81
|
+
}
|
|
82
|
+
if (entry.isSymbolicLink()) {
|
|
83
|
+
throw ipcDirectoryError('IPC_DIR_SYMLINK', `${label} must not be a symlink`);
|
|
84
|
+
}
|
|
85
|
+
if (!entry.isDirectory()) {
|
|
86
|
+
throw ipcDirectoryError('IPC_DIR_NOT_DIRECTORY', `${label} must be a directory`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const canonical = existingRealpath(candidate);
|
|
90
|
+
if (!canonical || canonical !== candidate) {
|
|
91
|
+
throw ipcDirectoryError('IPC_DIR_NOT_CANONICAL', `${label} must be canonical and contain no symlinks`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const stat = fs.statSync(candidate);
|
|
95
|
+
if (stat.uid !== currentUid()) {
|
|
96
|
+
throw ipcDirectoryError('IPC_DIR_WRONG_OWNER', `${label} must be owned by the current user`);
|
|
97
|
+
}
|
|
98
|
+
const mode = stat.mode & 0o777;
|
|
99
|
+
if (exactMode !== null && mode !== exactMode) {
|
|
100
|
+
throw ipcDirectoryError('IPC_DIR_WRONG_MODE', `${label} must have mode 0700`);
|
|
101
|
+
}
|
|
102
|
+
return stat;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertSafeCreationParent(candidate) {
|
|
106
|
+
const parent = path.dirname(candidate);
|
|
107
|
+
const stat = assertOwnedDirectory(parent, { label: 'IPC directory parent' });
|
|
108
|
+
if ((stat.mode & 0o022) !== 0) {
|
|
109
|
+
throw ipcDirectoryError(
|
|
110
|
+
'IPC_DIR_UNSAFE_PARENT',
|
|
111
|
+
'IPC directory parent must not be group- or world-writable',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the one IPC runtime directory used by the daemon and its clients.
|
|
118
|
+
*
|
|
119
|
+
* The default stays under Polygram's DATA_DIR (`process.cwd()`), which is
|
|
120
|
+
* already outside agent write access. An override is accepted only as an
|
|
121
|
+
* absolute, canonical, non-temporary path.
|
|
122
|
+
*/
|
|
123
|
+
function runtimeDirectory({ cwd = process.cwd(), env = process.env } = {}) {
|
|
124
|
+
const hasOverride = Object.prototype.hasOwnProperty.call(env, 'POLYGRAM_IPC_DIR');
|
|
125
|
+
const configured = hasOverride
|
|
126
|
+
? env.POLYGRAM_IPC_DIR
|
|
127
|
+
: path.join(cwd, DEFAULT_DIRECTORY_NAME);
|
|
128
|
+
|
|
129
|
+
if (typeof configured !== 'string' || configured.length === 0) {
|
|
130
|
+
throw ipcDirectoryError('IPC_DIR_INVALID', 'IPC directory must be a non-empty path');
|
|
131
|
+
}
|
|
132
|
+
if (!path.isAbsolute(configured)) {
|
|
133
|
+
throw ipcDirectoryError('IPC_DIR_RELATIVE', 'IPC directory must be absolute');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resolved = path.resolve(configured);
|
|
137
|
+
if (resolved !== configured) {
|
|
138
|
+
throw ipcDirectoryError('IPC_DIR_NOT_CANONICAL', 'IPC directory path must be canonical');
|
|
139
|
+
}
|
|
140
|
+
assertNonTemporary(resolved);
|
|
141
|
+
assertSafeCreationParent(resolved);
|
|
142
|
+
|
|
143
|
+
if (fs.existsSync(resolved)) {
|
|
144
|
+
assertOwnedDirectory(resolved, { exactMode: 0o700 });
|
|
145
|
+
}
|
|
146
|
+
return resolved;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create the resolved runtime directory exactly once with owner-only access,
|
|
151
|
+
* then revalidate it to catch pre-existing or raced unsafe filesystem state.
|
|
152
|
+
*/
|
|
153
|
+
function ensureRuntimeDirectory(options) {
|
|
154
|
+
const directory = runtimeDirectory(options);
|
|
155
|
+
if (!fs.existsSync(directory)) {
|
|
156
|
+
try {
|
|
157
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error.code !== 'EEXIST') {
|
|
160
|
+
throw ipcDirectoryError('IPC_DIR_CREATE_FAILED', 'IPC directory could not be created');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
assertOwnedDirectory(directory, { exactMode: 0o700 });
|
|
165
|
+
return directory;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateBotName(botName) {
|
|
169
|
+
if (
|
|
170
|
+
typeof botName !== 'string'
|
|
171
|
+
|| Buffer.byteLength(botName, 'utf8') > MAX_BOT_NAME_BYTES
|
|
172
|
+
|| !BOT_NAME_PATTERN.test(botName)
|
|
173
|
+
) {
|
|
174
|
+
throw ipcDirectoryError(
|
|
175
|
+
'IPC_BOT_NAME_INVALID',
|
|
176
|
+
'IPC bot name may contain only letters, numbers, underscores, and hyphens',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return botName;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function socketPathFor(botName, options) {
|
|
183
|
+
const safeBotName = validateBotName(botName);
|
|
184
|
+
const socketPath = path.join(
|
|
185
|
+
runtimeDirectory(options),
|
|
186
|
+
`polygram-${safeBotName}.sock`,
|
|
187
|
+
);
|
|
188
|
+
if (Buffer.byteLength(socketPath, 'utf8') > MAX_UNIX_SOCKET_PATH_BYTES) {
|
|
189
|
+
throw ipcDirectoryError(
|
|
190
|
+
'IPC_SOCKET_PATH_TOO_LONG',
|
|
191
|
+
'IPC socket path exceeds the portable Unix-domain socket limit',
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return socketPath;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function secretPathFor(botName, options) {
|
|
198
|
+
const safeBotName = validateBotName(botName);
|
|
199
|
+
return path.join(runtimeDirectory(options), `polygram-${safeBotName}.secret`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
runtimeDirectory,
|
|
204
|
+
ensureRuntimeDirectory,
|
|
205
|
+
socketPathFor,
|
|
206
|
+
secretPathFor,
|
|
207
|
+
validateBotName,
|
|
208
|
+
};
|
package/lib/ipc/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unix socket IPC server for the polygram daemon.
|
|
3
3
|
*
|
|
4
|
-
* One socket per bot process
|
|
4
|
+
* One socket per bot process in Polygram's owner-only IPC runtime directory. Clients
|
|
5
5
|
* (Claude Code approval hooks, future cron) send newline-delimited
|
|
6
6
|
* JSON requests; the server invokes a registered handler and writes back a
|
|
7
7
|
* newline-delimited JSON response.
|
|
@@ -13,14 +13,12 @@
|
|
|
13
13
|
const net = require('net');
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const crypto = require('crypto');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return `/tmp/polygram-${botName}.secret`;
|
|
23
|
-
}
|
|
16
|
+
const {
|
|
17
|
+
runtimeDirectory,
|
|
18
|
+
ensureRuntimeDirectory,
|
|
19
|
+
socketPathFor,
|
|
20
|
+
secretPathFor,
|
|
21
|
+
} = require('./runtime-directory');
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* Generate and persist a fresh IPC secret. Written 0600 so only same-UID
|
|
@@ -29,9 +27,10 @@ function secretPathFor(botName) {
|
|
|
29
27
|
* us a cheap way to reject stray processes that stumbled onto the socket
|
|
30
28
|
* without intent — and makes the auth model explicit rather than implicit.
|
|
31
29
|
*/
|
|
32
|
-
function writeSecret(botName) {
|
|
30
|
+
function writeSecret(botName, options) {
|
|
33
31
|
const secret = crypto.randomBytes(32).toString('base64url');
|
|
34
|
-
const p = secretPathFor(botName);
|
|
32
|
+
const p = secretPathFor(botName, options);
|
|
33
|
+
ensureRuntimeDirectory(options);
|
|
35
34
|
fs.writeFileSync(p, secret, { mode: 0o600 });
|
|
36
35
|
// Re-chmod in case umask overrode our mode on create.
|
|
37
36
|
try { fs.chmodSync(p, 0o600); } catch {}
|
|
@@ -146,4 +145,11 @@ function writeReply(sock, obj) {
|
|
|
146
145
|
} catch {}
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
module.exports = {
|
|
148
|
+
module.exports = {
|
|
149
|
+
start,
|
|
150
|
+
runtimeDirectory,
|
|
151
|
+
ensureRuntimeDirectory,
|
|
152
|
+
socketPathFor,
|
|
153
|
+
secretPathFor,
|
|
154
|
+
writeSecret,
|
|
155
|
+
};
|
package/lib/model-costs.js
CHANGED
|
@@ -28,10 +28,11 @@ const MODEL_COSTS = {
|
|
|
28
28
|
'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreation: 1 },
|
|
29
29
|
// Claude Opus 4.7 (1M context)
|
|
30
30
|
'claude-opus-4-7': { input: 15, output: 75, cacheRead: 1.50, cacheCreation: 18.75 },
|
|
31
|
-
// Claude Opus 4.8 —
|
|
32
|
-
//
|
|
33
|
-
// opus turns fall back to the Sonnet `default` and undercount cost ~5×.
|
|
31
|
+
// Claude Opus 4.8 — kept for turns recorded before the CLI pin moved to
|
|
32
|
+
// 2.1.220; without an entry those fall back to the Sonnet `default`.
|
|
34
33
|
'claude-opus-4-8': { input: 15, output: 75, cacheRead: 1.50, cacheCreation: 18.75 },
|
|
34
|
+
// Claude Opus 5 — what `--model opus` resolves to on the pinned CLI.
|
|
35
|
+
'claude-opus-5': { input: 5, output: 25, cacheRead: 0.50, cacheCreation: 6.25 },
|
|
35
36
|
// Default fallback — Sonnet rates (safest mid-tier estimate).
|
|
36
37
|
default: { input: 3, output: 15, cacheRead: 0.30, cacheCreation: 3.75 },
|
|
37
38
|
};
|
package/lib/pm-interface.js
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
* @property {string} sessionKey
|
|
23
23
|
* @property {string|null} chatId
|
|
24
24
|
* @property {string|null} threadId
|
|
25
|
+
* @property {string} label
|
|
26
|
+
* @property {'claude'|'codex'} runtime
|
|
27
|
+
* @property {string} backend
|
|
28
|
+
* @property {string|null} spawnProfileId
|
|
25
29
|
* @property {boolean} closed
|
|
26
30
|
* @property {boolean} inFlight
|
|
27
31
|
* @property {Array<object>} pendingQueue — array of pending sends
|
|
@@ -53,6 +57,24 @@
|
|
|
53
57
|
* @property {string|null} chatId
|
|
54
58
|
* @property {string|null} threadId
|
|
55
59
|
* @property {string} label
|
|
60
|
+
* @property {'claude'|'codex'} runtime
|
|
61
|
+
* Canonical runtime selected before process creation. Explicit Codex
|
|
62
|
+
* selection reaches Orchestra through this field; it is not passed to
|
|
63
|
+
* Claude's pickBackend() compatibility resolver.
|
|
64
|
+
* @property {string} backend
|
|
65
|
+
* Canonical provider backend (`sdk`, `cli`, or `codex`).
|
|
66
|
+
* @property {string|null} spawnProfileId
|
|
67
|
+
* Opaque identity from the successful runtime preflight. Codex process
|
|
68
|
+
* creation must receive the exact profile it was preflighted with.
|
|
69
|
+
* @property {string} [runtimeConfigIdentity]
|
|
70
|
+
* Polygram-owned identity of its resolved configuration. Orchestra ignores
|
|
71
|
+
* this field and enforces runtime/backend/spawnProfileId with its own
|
|
72
|
+
* identity; Polygram may use it for config audit or UI comparisons.
|
|
73
|
+
* @property {string|null} [cwd]
|
|
74
|
+
* Resolved topic/chat/default working directory included in spawn identity.
|
|
75
|
+
* @property {string|null} [agent]
|
|
76
|
+
* Resolved Claude agent. Always null for Codex; Codex instructions are
|
|
77
|
+
* attested independently and must not reuse Claude's named-agent bundle.
|
|
56
78
|
*
|
|
57
79
|
* @typedef {object} Pm
|
|
58
80
|
* The unified ProcessManager interface.
|
|
@@ -61,6 +83,12 @@
|
|
|
61
83
|
* @property {(sessionKey: string) => boolean} has
|
|
62
84
|
* @property {(sessionKey: string) => PmEntry|null} get
|
|
63
85
|
* @property {(sessionKey: string, ctx: PmSpawnContext) => Promise<PmEntry>} getOrSpawn
|
|
86
|
+
* @property {(sessionKey: string, ctx: PmSpawnContext) => Promise<PmEntry>} replaceRuntime
|
|
87
|
+
* — strict serialized replacement. Same identity returns the current entry.
|
|
88
|
+
* Live/pinned/blocked or conflicting replacement throws
|
|
89
|
+
* `RUNTIME_SWITCH_IN_FLIGHT`; unsafe retirement throws
|
|
90
|
+
* `RUNTIME_SWITCH_EVICTION_FAILED`; unknown runtime throws
|
|
91
|
+
* `RUNTIME_UNKNOWN`; provider lease/config errors propagate.
|
|
64
92
|
* @property {(sessionKey: string, prompt: string, opts?: PmSendOptions) => Promise<PmSendResult>} send
|
|
65
93
|
* @property {(sessionKey: string) => Promise<void>} kill
|
|
66
94
|
* @property {(chatId: string|number) => Promise<void>} killChat
|
|
@@ -76,6 +104,11 @@
|
|
|
76
104
|
* Returns false when sessionKey not found.
|
|
77
105
|
* @property {((sessionKey: string, model: string) => Promise<boolean>)=} setModel
|
|
78
106
|
* — Query.setModel live (no respawn).
|
|
107
|
+
* @property {((sessionKey: string, settings: {model: string, effort: string}) => Promise<object>)=} selectModelSettings
|
|
108
|
+
* @property {((sessionKey: string) => Promise<object>)=} getModelSettingsStatus
|
|
109
|
+
* — Atomically select the complete Codex pair for the next admitted turn.
|
|
110
|
+
* Expected lifecycle states return a discriminated `outcome`; malformed
|
|
111
|
+
* inputs and unexpected internal failures reject.
|
|
79
112
|
* @property {((sessionKey: string, settings: {effortLevel?: string}) => Promise<boolean>)=} applyFlagSettings
|
|
80
113
|
* — Query.applyFlagSettings live (no respawn).
|
|
81
114
|
* @property {((sessionKey: string, mode: string) => Promise<boolean>)=} setPermissionMode
|
package/lib/prompt.js
CHANGED
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
* Telegram payload → polygram DB → unresolvable marker.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const { resolveRuntimeDescriptor } = require('./runtime-config');
|
|
9
9
|
|
|
10
10
|
function resolvePromptBackend({ config, chatId, threadId = null }) {
|
|
11
|
-
|
|
11
|
+
const normalizedChatId = String(chatId);
|
|
12
|
+
const normalizedThreadId = threadId == null || threadId === ''
|
|
13
|
+
? null
|
|
14
|
+
: String(threadId);
|
|
15
|
+
return resolveRuntimeDescriptor({
|
|
12
16
|
config,
|
|
13
|
-
chatId:
|
|
14
|
-
threadId:
|
|
15
|
-
|
|
16
|
-
});
|
|
17
|
+
chatId: normalizedChatId,
|
|
18
|
+
threadId: normalizedThreadId,
|
|
19
|
+
defaultPm: 'sdk',
|
|
20
|
+
}).backend;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -22,13 +26,15 @@ function resolvePromptBackend({ config, chatId, threadId = null }) {
|
|
|
22
26
|
* must not be told to emit stickers it doesn't have. The emoji list is the bot's OWN
|
|
23
27
|
* pack (its emojiToSticker keys), not a hard-coded set, so it always matches reality.
|
|
24
28
|
*
|
|
25
|
-
* @param {{ stickerEmojis?: string[], backend?: 'sdk'|'cli' }} [opts]
|
|
29
|
+
* @param {{ stickerEmojis?: string[], backend?: 'sdk'|'cli'|'codex' }} [opts]
|
|
26
30
|
*/
|
|
27
31
|
function polygramInfo({ stickerEmojis = [], backend = 'sdk' } = {}) {
|
|
28
32
|
const hasStickers = stickerEmojis.length > 0;
|
|
29
33
|
const deliveryContract = backend === 'cli'
|
|
30
34
|
? `You are connected via a Telegram daemon (polygram) in channels mode. Inline final text is invisible to the Telegram user. For every user-visible response, you MUST call \`mcp__polygram-bridge__reply\` within this turn. Only a successful reply-tool receipt proves delivery. Do not claim something was already sent or posted above based only on the transcript; transcript content is not a delivery receipt. If delivery is uncertain, send it now with the reply tool.`
|
|
31
|
-
:
|
|
35
|
+
: backend === 'codex'
|
|
36
|
+
? `You are connected to Telegram through polygram's Codex app-server runtime. Reply with inline text — polygram streams and delivers your final response automatically. Do NOT use Telegram MCP tools. This is a native macOS beta: command network and model-native web search are disabled, and product MCP tools and interactive approvals are unavailable. Keep long-running commands in the foreground. Do not start detached or background servers; they are unsupported and may survive hard runtime loss.`
|
|
37
|
+
: `You are connected via a Telegram daemon (polygram). Just reply with text — polygram delivers your response automatically. Do NOT use Telegram MCP tools.`;
|
|
32
38
|
const emojiLine = hasStickers
|
|
33
39
|
? `Single emoji reply = auto-converted: ${stickerEmojis.join('')} become your stickers, any other emoji (🔥👍💪❤️) becomes a reaction on the user's message.`
|
|
34
40
|
: `Single emoji reply = auto-converted to a reaction on the user's message (any Telegram emoji: 🔥👍💪❤️ …).`;
|
|
@@ -196,7 +202,7 @@ function buildVoiceTags(attachments) {
|
|
|
196
202
|
* @param {string} params.sessionCtx - session context file contents (optional)
|
|
197
203
|
* @param {Array} params.attachments - downloaded attachments
|
|
198
204
|
* @param {Object} params.replyTo - input for buildReplyToBlock (optional)
|
|
199
|
-
* @param {'sdk'|'cli'} params.backend - delivery contract for the selected process backend
|
|
205
|
+
* @param {'sdk'|'cli'|'codex'} params.backend - delivery contract for the selected process backend
|
|
200
206
|
*/
|
|
201
207
|
function buildPrompt({ msg, topicName = '', sessionCtx = '', attachments = [], replyTo = null, polygramHistory = '', stickerEmojis = [], backend = 'sdk' }) {
|
|
202
208
|
const chatId = msg.chat.id.toString();
|
package/lib/rewind/rewind.js
CHANGED
|
@@ -148,8 +148,28 @@ function createRewindHandler({ pm, tg, bot, botName = 'bot', executeRewind, logE
|
|
|
148
148
|
* Dispatcher hook. Returns { consumed }. Consumes any `/rewind` (valid → queued, invalid →
|
|
149
149
|
* the operator is told why) so it never starts a normal turn.
|
|
150
150
|
*/
|
|
151
|
-
async function tryConsume({
|
|
151
|
+
async function tryConsume({
|
|
152
|
+
sessionKey,
|
|
153
|
+
chatId,
|
|
154
|
+
threadId = null,
|
|
155
|
+
msg,
|
|
156
|
+
cleanText,
|
|
157
|
+
runtime = 'claude',
|
|
158
|
+
botUsername,
|
|
159
|
+
rewindSafe,
|
|
160
|
+
isOperatorIdentity,
|
|
161
|
+
paired,
|
|
162
|
+
accessMode,
|
|
163
|
+
}) {
|
|
152
164
|
if (!isRewindCommand(cleanText)) return { consumed: false };
|
|
165
|
+
if (runtime === 'codex') {
|
|
166
|
+
await ack(
|
|
167
|
+
chatId,
|
|
168
|
+
threadId,
|
|
169
|
+
'Codex thread rewind is not supported yet; the active Codex thread was unchanged.',
|
|
170
|
+
);
|
|
171
|
+
return { consumed: true };
|
|
172
|
+
}
|
|
153
173
|
const gate = gateRewindRequest({ msg, botUsername, rewindSafe, isOperatorIdentity, paired, accessMode });
|
|
154
174
|
if (!gate.ok) {
|
|
155
175
|
await ack(chatId, threadId, `↩️ ${gate.reason}`);
|