polygram 0.22.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/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
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
'use strict';
|
|
29
29
|
|
|
30
30
|
const { getConfigWriteScope } = require('../session-key');
|
|
31
|
+
const {
|
|
32
|
+
configuredRuntimeValue,
|
|
33
|
+
formatCodexSettingsOutcome,
|
|
34
|
+
getCodexCatalogEfforts,
|
|
35
|
+
getCodexCatalogModels,
|
|
36
|
+
isCodexRuntimeView,
|
|
37
|
+
resolveCodexEffortForModel,
|
|
38
|
+
} = require('./config-ui');
|
|
31
39
|
|
|
32
40
|
function createSlashCommands({
|
|
33
41
|
config,
|
|
@@ -42,6 +50,8 @@ function createSlashCommands({
|
|
|
42
50
|
getOrSpawnForChat,
|
|
43
51
|
parsePairCodeArgs,
|
|
44
52
|
modelVersionsDesc,
|
|
53
|
+
resolveRuntimeView = null,
|
|
54
|
+
intentLock,
|
|
45
55
|
saveConfig = () => {},
|
|
46
56
|
botName,
|
|
47
57
|
logEvent,
|
|
@@ -61,6 +71,30 @@ function createSlashCommands({
|
|
|
61
71
|
// entry.query.getContextUsage directly, which silently said "No
|
|
62
72
|
// active session yet" on tmux even when the chat was alive.
|
|
63
73
|
if (botAllowsCommands && text === '/context') {
|
|
74
|
+
if (typeof resolveRuntimeView === 'function') {
|
|
75
|
+
try {
|
|
76
|
+
const runtimeView = await resolveRuntimeView({
|
|
77
|
+
sessionKey,
|
|
78
|
+
chatId,
|
|
79
|
+
threadId: threadIdStr,
|
|
80
|
+
});
|
|
81
|
+
if (isCodexRuntimeView(runtimeView)) {
|
|
82
|
+
await sendReply(
|
|
83
|
+
'Codex context usage reporting is not supported yet; the active Codex thread was unchanged.',
|
|
84
|
+
);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.error?.(
|
|
89
|
+
`[${label}] /context runtime view failed: `
|
|
90
|
+
+ `${error?.code || error?.name || 'unknown'}`,
|
|
91
|
+
);
|
|
92
|
+
await sendReply(
|
|
93
|
+
'Context status is temporarily unavailable; the active session was not changed.',
|
|
94
|
+
);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
64
98
|
if (!pm.has(sessionKey)) {
|
|
65
99
|
await sendReply('📚 No active session yet — send a message first, then /context.');
|
|
66
100
|
return true;
|
|
@@ -85,6 +119,31 @@ function createSlashCommands({
|
|
|
85
119
|
// was LRU-evicted but DB has a saved session_id, auto-spawn
|
|
86
120
|
// with --resume so /compact has something to work with.
|
|
87
121
|
if (botAllowsCommands && text.startsWith('/compact')) {
|
|
122
|
+
if (typeof resolveRuntimeView === 'function') {
|
|
123
|
+
let runtimeView;
|
|
124
|
+
try {
|
|
125
|
+
runtimeView = await resolveRuntimeView({
|
|
126
|
+
sessionKey,
|
|
127
|
+
chatId,
|
|
128
|
+
threadId: threadIdStr,
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.error?.(
|
|
132
|
+
`[${label}] /compact runtime view failed: `
|
|
133
|
+
+ `${error?.code || error?.name || 'unknown'}`,
|
|
134
|
+
);
|
|
135
|
+
await sendReply(
|
|
136
|
+
'Compaction status is temporarily unavailable; the active session was not changed.',
|
|
137
|
+
);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (isCodexRuntimeView(runtimeView)) {
|
|
141
|
+
await sendReply(
|
|
142
|
+
'Codex thread compaction is not supported yet; the active Codex thread was unchanged.',
|
|
143
|
+
);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
88
147
|
let entry = pm.get(sessionKey);
|
|
89
148
|
if (!entry) {
|
|
90
149
|
const savedSessionId = getClaudeSessionId(db, sessionKey);
|
|
@@ -145,33 +204,96 @@ function createSlashCommands({
|
|
|
145
204
|
// /reload → kill closes Query, session_id preserved → same
|
|
146
205
|
// conversation continues with fresh agent/skill code
|
|
147
206
|
if (botAllowsCommands && text === '/reload') {
|
|
207
|
+
let runtimeView = null;
|
|
208
|
+
if (typeof resolveRuntimeView === 'function') {
|
|
209
|
+
try {
|
|
210
|
+
runtimeView = await resolveRuntimeView({
|
|
211
|
+
sessionKey,
|
|
212
|
+
chatId,
|
|
213
|
+
threadId: threadIdStr,
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.error?.(
|
|
217
|
+
`[${label}] /reload runtime view failed: `
|
|
218
|
+
+ `${error?.code || error?.name || 'unknown'}`,
|
|
219
|
+
);
|
|
220
|
+
await sendReply(
|
|
221
|
+
'Runtime status is temporarily unavailable; the active session was not changed.',
|
|
222
|
+
);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const codex = isCodexRuntimeView(runtimeView);
|
|
148
227
|
if (pm.has(sessionKey)) {
|
|
149
|
-
try {
|
|
150
|
-
|
|
228
|
+
try {
|
|
229
|
+
await pm.kill(sessionKey);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
logger.error?.(`[${label}] kill on /reload: ${err.message}`);
|
|
232
|
+
await sendReply(
|
|
233
|
+
codex
|
|
234
|
+
? '🔄 Couldn’t reload Codex because cleanup was not proven; the active Codex thread may be unchanged.'
|
|
235
|
+
: '🔄 Couldn’t reload Claude; the active session may be unchanged.',
|
|
236
|
+
);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
151
239
|
}
|
|
152
240
|
logEvent('session-reload-command', {
|
|
153
241
|
chat_id: chatId, command: text,
|
|
154
242
|
user: cmdUser, user_id: cmdUserId,
|
|
155
243
|
});
|
|
156
|
-
await sendReply(
|
|
244
|
+
await sendReply(
|
|
245
|
+
codex
|
|
246
|
+
? '🔄 Reloaded Codex. The next message resumes the same Codex thread with a fresh runtime.'
|
|
247
|
+
: '🔄 Reloaded. Next message picks up the conversation with fresh skills/agents.',
|
|
248
|
+
);
|
|
157
249
|
return true;
|
|
158
250
|
}
|
|
159
251
|
|
|
160
252
|
// /new + /reset + /clear — fresh session (all synonyms)
|
|
161
253
|
if (botAllowsCommands && (text === '/new' || text === '/reset' || text === '/clear')) {
|
|
254
|
+
let runtimeView = null;
|
|
255
|
+
if (typeof resolveRuntimeView === 'function') {
|
|
256
|
+
try {
|
|
257
|
+
runtimeView = await resolveRuntimeView({
|
|
258
|
+
sessionKey,
|
|
259
|
+
chatId,
|
|
260
|
+
threadId: threadIdStr,
|
|
261
|
+
});
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logger.error?.(
|
|
264
|
+
`[${label}] ${text} runtime view failed: `
|
|
265
|
+
+ `${error?.code || error?.name || 'unknown'}`,
|
|
266
|
+
);
|
|
267
|
+
await sendReply(
|
|
268
|
+
'Runtime status is temporarily unavailable; the active session was not changed.',
|
|
269
|
+
);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const codex = isCodexRuntimeView(runtimeView);
|
|
162
274
|
let drained = 0;
|
|
163
275
|
try {
|
|
164
276
|
const r = await pm.resetSession(sessionKey, { reason: text.slice(1) });
|
|
165
277
|
drained = r?.drainedPendings ?? 0;
|
|
166
278
|
} catch (err) {
|
|
167
279
|
logger.error?.(`[${label}] resetSession ${text}: ${err.message}`);
|
|
280
|
+
await sendReply(
|
|
281
|
+
codex
|
|
282
|
+
? '✨ Couldn’t start a fresh Codex thread because cleanup was not proven; the prior Codex thread may be unchanged.'
|
|
283
|
+
: '✨ Couldn’t start a fresh Claude session; the prior session may be unchanged.',
|
|
284
|
+
);
|
|
285
|
+
return true;
|
|
168
286
|
}
|
|
169
287
|
contextHintShown.delete(sessionKey);
|
|
170
288
|
logEvent('session-reset-command', {
|
|
171
289
|
chat_id: chatId, command: text, drained_pendings: drained,
|
|
172
290
|
user: cmdUser, user_id: cmdUserId,
|
|
173
291
|
});
|
|
174
|
-
await sendReply(
|
|
292
|
+
await sendReply(
|
|
293
|
+
codex
|
|
294
|
+
? '✨ Started a fresh Codex thread.'
|
|
295
|
+
: '✨ Started a fresh session.',
|
|
296
|
+
);
|
|
175
297
|
return true;
|
|
176
298
|
}
|
|
177
299
|
|
|
@@ -216,47 +338,331 @@ function createSlashCommands({
|
|
|
216
338
|
// /model X
|
|
217
339
|
if (botAllowsCommands && text.startsWith('/model ')) {
|
|
218
340
|
const newModel = text.slice(7).trim();
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
341
|
+
const releaseIntent = await intentLock.acquire(sessionKey);
|
|
342
|
+
let replyText;
|
|
343
|
+
try {
|
|
344
|
+
let runtimeView = null;
|
|
345
|
+
if (typeof resolveRuntimeView === 'function') {
|
|
346
|
+
try {
|
|
347
|
+
runtimeView = await resolveRuntimeView({
|
|
348
|
+
sessionKey,
|
|
349
|
+
chatId,
|
|
350
|
+
threadId: threadIdStr,
|
|
351
|
+
});
|
|
352
|
+
} catch (error) {
|
|
353
|
+
logger.error?.(
|
|
354
|
+
`[${label}] /model runtime view failed: `
|
|
355
|
+
+ `${error?.code || error?.name || 'unknown'}`,
|
|
356
|
+
);
|
|
357
|
+
replyText = 'Model settings are temporarily unavailable.';
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (!replyText) {
|
|
361
|
+
const codex = isCodexRuntimeView(runtimeView);
|
|
362
|
+
const validModels = codex
|
|
363
|
+
? getCodexCatalogModels(runtimeView).map((entry) => entry.model)
|
|
364
|
+
: ['opus', 'sonnet', 'haiku'];
|
|
365
|
+
if (!validModels.includes(newModel)) {
|
|
366
|
+
replyText =
|
|
367
|
+
`Unknown model. Use: ${validModels.join(', ')
|
|
368
|
+
|| 'no authenticated models available'}`;
|
|
369
|
+
} else {
|
|
370
|
+
const { scope: wScope, threadId: wThread } =
|
|
371
|
+
getConfigWriteScope(chatConfig, threadIdStr);
|
|
372
|
+
const configField = codex ? 'codexModel' : 'model';
|
|
373
|
+
const oldModel = wScope[configField] != null
|
|
374
|
+
? wScope[configField]
|
|
375
|
+
: chatConfig[configField] != null
|
|
376
|
+
? chatConfig[configField]
|
|
377
|
+
: codex ? runtimeView.model : chatConfig[configField];
|
|
378
|
+
const snapshots = new Map([
|
|
379
|
+
[configField, {
|
|
380
|
+
present: Object.hasOwn(wScope, configField),
|
|
381
|
+
value: wScope[configField],
|
|
382
|
+
}],
|
|
383
|
+
['codexEffort', {
|
|
384
|
+
present: Object.hasOwn(wScope, 'codexEffort'),
|
|
385
|
+
value: wScope.codexEffort,
|
|
386
|
+
}],
|
|
387
|
+
]);
|
|
388
|
+
let effortAdjustment = null;
|
|
389
|
+
let nextEffort = null;
|
|
390
|
+
if (codex) {
|
|
391
|
+
const oldEffort = wScope.codexEffort != null
|
|
392
|
+
? wScope.codexEffort
|
|
393
|
+
: chatConfig.codexEffort != null
|
|
394
|
+
? chatConfig.codexEffort
|
|
395
|
+
: runtimeView.effort;
|
|
396
|
+
nextEffort = resolveCodexEffortForModel(
|
|
397
|
+
runtimeView,
|
|
398
|
+
newModel,
|
|
399
|
+
oldEffort,
|
|
400
|
+
);
|
|
401
|
+
if (nextEffort == null) {
|
|
402
|
+
replyText =
|
|
403
|
+
'Selected model has no authenticated default effort.';
|
|
404
|
+
} else if (nextEffort !== oldEffort) {
|
|
405
|
+
effortAdjustment = {
|
|
406
|
+
oldValue: oldEffort,
|
|
407
|
+
newValue: nextEffort,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (!replyText) {
|
|
412
|
+
wScope[configField] = newModel;
|
|
413
|
+
if (effortAdjustment) {
|
|
414
|
+
wScope.codexEffort = effortAdjustment.newValue;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
await saveConfig();
|
|
418
|
+
} catch (error) {
|
|
419
|
+
for (const [field, snapshot] of snapshots) {
|
|
420
|
+
if (snapshot.present) wScope[field] = snapshot.value;
|
|
421
|
+
else delete wScope[field];
|
|
422
|
+
}
|
|
423
|
+
logger.error?.(
|
|
424
|
+
`[${botName}] /model saveConfig failed: ${error.message}`,
|
|
425
|
+
);
|
|
426
|
+
replyText =
|
|
427
|
+
"Couldn't save the model selection; nothing changed.";
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (!replyText) {
|
|
431
|
+
const auditRows = [{
|
|
432
|
+
chat_id: chatId, thread_id: wThread, field: 'model',
|
|
433
|
+
old_value: oldModel, new_value: newModel,
|
|
434
|
+
user: cmdUser, user_id: cmdUserId, source: 'command',
|
|
435
|
+
}];
|
|
436
|
+
if (effortAdjustment) {
|
|
437
|
+
auditRows.push({
|
|
438
|
+
chat_id: chatId,
|
|
439
|
+
thread_id: wThread,
|
|
440
|
+
field: 'effort',
|
|
441
|
+
old_value: effortAdjustment.oldValue,
|
|
442
|
+
new_value: effortAdjustment.newValue,
|
|
443
|
+
user: cmdUser,
|
|
444
|
+
user_id: cmdUserId,
|
|
445
|
+
source: 'command',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
if (codex) {
|
|
449
|
+
try {
|
|
450
|
+
db.logConfigChanges(auditRows);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
for (const [field, snapshot] of snapshots) {
|
|
453
|
+
if (snapshot.present) wScope[field] = snapshot.value;
|
|
454
|
+
else delete wScope[field];
|
|
455
|
+
}
|
|
456
|
+
let rollbackFailed = false;
|
|
457
|
+
try {
|
|
458
|
+
await saveConfig();
|
|
459
|
+
} catch (rollbackError) {
|
|
460
|
+
rollbackFailed = true;
|
|
461
|
+
logger.error?.(
|
|
462
|
+
`[${botName}] /model config rollback failed: `
|
|
463
|
+
+ rollbackError.message,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
logger.error?.(
|
|
467
|
+
`[${botName}] /model audit failed: ${error.message}`,
|
|
468
|
+
);
|
|
469
|
+
replyText = rollbackFailed
|
|
470
|
+
? "Couldn't audit the model selection; the live process "
|
|
471
|
+
+ 'was unchanged, but persisted config needs attention.'
|
|
472
|
+
: "Couldn't audit the model selection; nothing changed.";
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
dbWrite(() => {
|
|
476
|
+
for (const row of auditRows) db.logConfigChange(row);
|
|
477
|
+
}, 'log model change');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!replyText) {
|
|
481
|
+
let settingsResult = null;
|
|
482
|
+
let anyActive = false;
|
|
483
|
+
if (codex) {
|
|
484
|
+
try {
|
|
485
|
+
settingsResult = await pm.selectModelSettings(sessionKey, {
|
|
486
|
+
model: newModel,
|
|
487
|
+
effort: nextEffort,
|
|
488
|
+
});
|
|
489
|
+
} catch (error) {
|
|
490
|
+
logger.error?.(
|
|
491
|
+
`[${label}] Codex /model live selection failed: `
|
|
492
|
+
+ `${error?.code || error?.name || 'unknown'}`,
|
|
493
|
+
);
|
|
494
|
+
settingsResult = {
|
|
495
|
+
outcome: 'live-status-unknown',
|
|
496
|
+
nextTurn: { model: newModel, effort: nextEffort },
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
({ anyActive } = await applyConfigChange('model', newModel));
|
|
501
|
+
}
|
|
502
|
+
const catalogEntry = codex
|
|
503
|
+
? getCodexCatalogModels(runtimeView)
|
|
504
|
+
.find((entry) => entry.model === newModel)
|
|
505
|
+
: null;
|
|
506
|
+
const ver = codex
|
|
507
|
+
? catalogEntry?.displayName || newModel
|
|
508
|
+
: (modelVersionsDesc && modelVersionsDesc[newModel])
|
|
509
|
+
|| newModel;
|
|
510
|
+
const suffix = codex
|
|
511
|
+
? formatCodexSettingsOutcome(settingsResult)
|
|
512
|
+
: cliAwareSuffix(anyActive);
|
|
513
|
+
const effortNote = effortAdjustment
|
|
514
|
+
? `; Effort → ${effortAdjustment.newValue}`
|
|
515
|
+
: '';
|
|
516
|
+
replyText =
|
|
517
|
+
`Model → ${newModel} (${ver})${effortNote}${suffix}`;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} finally {
|
|
522
|
+
releaseIntent();
|
|
237
523
|
}
|
|
524
|
+
await sendReply(replyText);
|
|
238
525
|
return true;
|
|
239
526
|
}
|
|
240
527
|
|
|
241
528
|
// /effort X
|
|
242
529
|
if (botAllowsCommands && text.startsWith('/effort ')) {
|
|
243
530
|
const newEffort = text.slice(8).trim();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
531
|
+
const releaseIntent = await intentLock.acquire(sessionKey);
|
|
532
|
+
let replyText;
|
|
533
|
+
try {
|
|
534
|
+
let runtimeView = null;
|
|
535
|
+
if (typeof resolveRuntimeView === 'function') {
|
|
536
|
+
try {
|
|
537
|
+
runtimeView = await resolveRuntimeView({
|
|
538
|
+
sessionKey,
|
|
539
|
+
chatId,
|
|
540
|
+
threadId: threadIdStr,
|
|
541
|
+
});
|
|
542
|
+
} catch (error) {
|
|
543
|
+
logger.error?.(
|
|
544
|
+
`[${label}] /effort runtime view failed: `
|
|
545
|
+
+ `${error?.code || error?.name || 'unknown'}`,
|
|
546
|
+
);
|
|
547
|
+
replyText = 'Effort settings are temporarily unavailable.';
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (!replyText) {
|
|
551
|
+
const codex = isCodexRuntimeView(runtimeView);
|
|
552
|
+
const topicConfig = threadIdStr
|
|
553
|
+
? chatConfig?.topics?.[threadIdStr]
|
|
554
|
+
: null;
|
|
555
|
+
const currentModel = configuredRuntimeValue(
|
|
556
|
+
chatConfig,
|
|
557
|
+
topicConfig,
|
|
558
|
+
runtimeView,
|
|
559
|
+
'model',
|
|
560
|
+
);
|
|
561
|
+
const validEfforts = codex
|
|
562
|
+
? getCodexCatalogEfforts(runtimeView, currentModel)
|
|
563
|
+
: ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
564
|
+
if (!validEfforts.includes(newEffort)) {
|
|
565
|
+
replyText =
|
|
566
|
+
`Unknown effort. Use: ${validEfforts.join(', ')
|
|
567
|
+
|| 'no authenticated efforts available'}`;
|
|
568
|
+
} else {
|
|
569
|
+
const { scope: wScope, threadId: wThread } =
|
|
570
|
+
getConfigWriteScope(chatConfig, threadIdStr);
|
|
571
|
+
const configField = codex ? 'codexEffort' : 'effort';
|
|
572
|
+
const oldEffort = wScope[configField] != null
|
|
573
|
+
? wScope[configField]
|
|
574
|
+
: chatConfig[configField] != null
|
|
575
|
+
? chatConfig[configField]
|
|
576
|
+
: codex ? runtimeView.effort : chatConfig[configField];
|
|
577
|
+
const snapshot = {
|
|
578
|
+
present: Object.hasOwn(wScope, configField),
|
|
579
|
+
value: wScope[configField],
|
|
580
|
+
};
|
|
581
|
+
wScope[configField] = newEffort;
|
|
582
|
+
try {
|
|
583
|
+
await saveConfig();
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (snapshot.present) wScope[configField] = snapshot.value;
|
|
586
|
+
else delete wScope[configField];
|
|
587
|
+
logger.error?.(
|
|
588
|
+
`[${botName}] /effort saveConfig failed: ${error.message}`,
|
|
589
|
+
);
|
|
590
|
+
replyText =
|
|
591
|
+
"Couldn't save the effort selection; nothing changed.";
|
|
592
|
+
}
|
|
593
|
+
if (!replyText) {
|
|
594
|
+
const auditRow = {
|
|
595
|
+
chat_id: chatId, thread_id: wThread, field: 'effort',
|
|
596
|
+
old_value: oldEffort, new_value: newEffort,
|
|
597
|
+
user: cmdUser, user_id: cmdUserId, source: 'command',
|
|
598
|
+
};
|
|
599
|
+
if (codex) {
|
|
600
|
+
try {
|
|
601
|
+
db.logConfigChanges([auditRow]);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (snapshot.present) wScope[configField] = snapshot.value;
|
|
604
|
+
else delete wScope[configField];
|
|
605
|
+
let rollbackFailed = false;
|
|
606
|
+
try {
|
|
607
|
+
await saveConfig();
|
|
608
|
+
} catch (rollbackError) {
|
|
609
|
+
rollbackFailed = true;
|
|
610
|
+
logger.error?.(
|
|
611
|
+
`[${botName}] /effort config rollback failed: `
|
|
612
|
+
+ rollbackError.message,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
logger.error?.(
|
|
616
|
+
`[${botName}] /effort audit failed: ${error.message}`,
|
|
617
|
+
);
|
|
618
|
+
replyText = rollbackFailed
|
|
619
|
+
? "Couldn't audit the effort selection; the live process "
|
|
620
|
+
+ 'was unchanged, but persisted config needs attention.'
|
|
621
|
+
: "Couldn't audit the effort selection; nothing changed.";
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
dbWrite(
|
|
625
|
+
() => db.logConfigChange(auditRow),
|
|
626
|
+
'log effort change',
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (!replyText) {
|
|
631
|
+
let settingsResult = null;
|
|
632
|
+
let anyActive = false;
|
|
633
|
+
if (codex) {
|
|
634
|
+
try {
|
|
635
|
+
settingsResult = await pm.selectModelSettings(sessionKey, {
|
|
636
|
+
model: currentModel,
|
|
637
|
+
effort: newEffort,
|
|
638
|
+
});
|
|
639
|
+
} catch (error) {
|
|
640
|
+
logger.error?.(
|
|
641
|
+
`[${label}] Codex /effort live selection failed: `
|
|
642
|
+
+ `${error?.code || error?.name || 'unknown'}`,
|
|
643
|
+
);
|
|
644
|
+
settingsResult = {
|
|
645
|
+
outcome: 'live-status-unknown',
|
|
646
|
+
nextTurn: { model: currentModel, effort: newEffort },
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
({ anyActive } = await applyConfigChange(
|
|
651
|
+
'effort',
|
|
652
|
+
newEffort,
|
|
653
|
+
));
|
|
654
|
+
}
|
|
655
|
+
const suffix = codex
|
|
656
|
+
? formatCodexSettingsOutcome(settingsResult)
|
|
657
|
+
: cliAwareSuffix(anyActive);
|
|
658
|
+
replyText = `Effort → ${newEffort}${suffix}`;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} finally {
|
|
663
|
+
releaseIntent();
|
|
259
664
|
}
|
|
665
|
+
await sendReply(replyText);
|
|
260
666
|
return true;
|
|
261
667
|
}
|
|
262
668
|
|
package/lib/ipc/client.js
CHANGED
|
@@ -8,27 +8,29 @@
|
|
|
8
8
|
|
|
9
9
|
const net = require('net');
|
|
10
10
|
const fs = require('fs');
|
|
11
|
+
const {
|
|
12
|
+
runtimeDirectory,
|
|
13
|
+
ensureRuntimeDirectory,
|
|
14
|
+
socketPathFor,
|
|
15
|
+
secretPathFor,
|
|
16
|
+
validateBotName,
|
|
17
|
+
} = require('./runtime-directory');
|
|
11
18
|
|
|
12
19
|
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000;
|
|
13
20
|
const DEFAULT_CALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
14
21
|
|
|
15
|
-
function socketPathFor(botName) {
|
|
16
|
-
return `/tmp/polygram-${botName}.sock`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function secretPathFor(botName) {
|
|
20
|
-
return `/tmp/polygram-${botName}.secret`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
22
|
/**
|
|
24
23
|
* Read the IPC secret for a bot. Prefers POLYGRAM_IPC_SECRET env var (set by
|
|
25
24
|
* polygram when spawning Claude subprocesses) over the file (used by
|
|
26
25
|
* cron and external callers that aren't polygram children). Missing secret
|
|
27
26
|
* is not an error — caller decides whether to send it.
|
|
28
27
|
*/
|
|
29
|
-
function readSecret(botName) {
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
function readSecret(botName, options) {
|
|
29
|
+
validateBotName(botName);
|
|
30
|
+
const env = options?.env || process.env;
|
|
31
|
+
if (env.POLYGRAM_IPC_SECRET) return env.POLYGRAM_IPC_SECRET;
|
|
32
|
+
const secretPath = secretPathFor(botName, options);
|
|
33
|
+
try { return fs.readFileSync(secretPath, 'utf8').trim(); }
|
|
32
34
|
catch { return null; }
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -111,4 +113,12 @@ async function tell(bot, method, params = {}, opts = {}) {
|
|
|
111
113
|
return res.result;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
module.exports = {
|
|
116
|
+
module.exports = {
|
|
117
|
+
call,
|
|
118
|
+
tell,
|
|
119
|
+
runtimeDirectory,
|
|
120
|
+
ensureRuntimeDirectory,
|
|
121
|
+
socketPathFor,
|
|
122
|
+
secretPathFor,
|
|
123
|
+
readSecret,
|
|
124
|
+
};
|