polygram 0.22.0 → 0.23.1
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
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure runtime selection for one chat/topic.
|
|
3
|
+
*
|
|
4
|
+
* Claude backend selection delegates to Orchestra's established resolver so
|
|
5
|
+
* aliases, warning behavior, truthy fallthrough, and unknown-value fallback do
|
|
6
|
+
* not drift. Codex is the sole fail-closed extension: it can be selected only
|
|
7
|
+
* with an availability object constructed from Orchestra's branded, asserted
|
|
8
|
+
* spawn-profile receipt.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { createHash } = require('crypto');
|
|
14
|
+
const { pickBackend } = require('@shumkov/orchestra');
|
|
15
|
+
|
|
16
|
+
const PM_DESCRIPTORS = Object.freeze({
|
|
17
|
+
sdk: Object.freeze({
|
|
18
|
+
provider: 'claude',
|
|
19
|
+
runtime: 'claude',
|
|
20
|
+
backend: 'sdk',
|
|
21
|
+
sessionNamespace: 'claude:inline',
|
|
22
|
+
promptMode: 'inline',
|
|
23
|
+
}),
|
|
24
|
+
cli: Object.freeze({
|
|
25
|
+
provider: 'claude',
|
|
26
|
+
runtime: 'claude',
|
|
27
|
+
backend: 'cli',
|
|
28
|
+
sessionNamespace: 'claude:channels',
|
|
29
|
+
promptMode: 'channels',
|
|
30
|
+
}),
|
|
31
|
+
codex: Object.freeze({
|
|
32
|
+
provider: 'codex',
|
|
33
|
+
runtime: 'codex',
|
|
34
|
+
backend: 'codex',
|
|
35
|
+
sessionNamespace: 'codex:app-server',
|
|
36
|
+
promptMode: 'codex',
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const CODEX_ERROR_BY_STATE = Object.freeze({
|
|
41
|
+
loading: Object.freeze({
|
|
42
|
+
code: 'CODEX_PREFLIGHT_LOADING',
|
|
43
|
+
action: 'Wait for Codex startup preflight to finish, then try again.',
|
|
44
|
+
}),
|
|
45
|
+
unavailable: Object.freeze({
|
|
46
|
+
code: 'CODEX_RUNTIME_UNAVAILABLE',
|
|
47
|
+
action: 'Fix Codex app-server credentials or startup preflight, then try again.',
|
|
48
|
+
}),
|
|
49
|
+
ineligible: Object.freeze({
|
|
50
|
+
code: 'CODEX_RUNTIME_INELIGIBLE',
|
|
51
|
+
action: 'Choose an eligible Codex account/model or keep this session on Claude.',
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const DEFAULT_CODEX_AVAILABILITY = Object.freeze({
|
|
56
|
+
state: 'unavailable',
|
|
57
|
+
reason: 'Codex runtime availability is not wired',
|
|
58
|
+
});
|
|
59
|
+
const MAX_CONFIG_VALUE_LENGTH = 4096;
|
|
60
|
+
const assertedReceiptByAvailability = new WeakMap();
|
|
61
|
+
|
|
62
|
+
class RuntimeConfigError extends Error {
|
|
63
|
+
constructor(message, {
|
|
64
|
+
code,
|
|
65
|
+
source = null,
|
|
66
|
+
value = null,
|
|
67
|
+
runtime = null,
|
|
68
|
+
availabilityState = null,
|
|
69
|
+
reason = null,
|
|
70
|
+
action = null,
|
|
71
|
+
}) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = 'RuntimeConfigError';
|
|
74
|
+
this.code = code;
|
|
75
|
+
this.source = source;
|
|
76
|
+
this.value = value;
|
|
77
|
+
this.runtime = runtime;
|
|
78
|
+
this.availabilityState = availabilityState;
|
|
79
|
+
this.reason = reason;
|
|
80
|
+
this.action = action;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convert an Orchestra-branded preflight receipt into the only availability
|
|
86
|
+
* object that can enable Codex. The assertion is required lazily so a
|
|
87
|
+
* Claude-only installation remains loadable when Codex exports are absent.
|
|
88
|
+
*/
|
|
89
|
+
function createCodexRuntimeAvailability(receipt) {
|
|
90
|
+
const orchestra = require('@shumkov/orchestra');
|
|
91
|
+
if (typeof orchestra.assertCodexSpawnProfile !== 'function') {
|
|
92
|
+
throw new RuntimeConfigError(
|
|
93
|
+
'Codex preflight receipt assertion is not installed.',
|
|
94
|
+
{
|
|
95
|
+
code: 'CODEX_PREFLIGHT_UNWIRED',
|
|
96
|
+
runtime: 'codex',
|
|
97
|
+
availabilityState: 'unavailable',
|
|
98
|
+
reason: 'Installed Orchestra does not expose assertCodexSpawnProfile',
|
|
99
|
+
action: 'Install the reviewed Orchestra Codex runtime before enabling Codex.',
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Do not emulate or partially duplicate Orchestra's private branding check.
|
|
105
|
+
// Invalid receipts retain Orchestra's CODEX_PREFLIGHT_RECEIPT_INVALID error.
|
|
106
|
+
const asserted = orchestra.assertCodexSpawnProfile(receipt);
|
|
107
|
+
if (asserted !== receipt) {
|
|
108
|
+
throw new RuntimeConfigError(
|
|
109
|
+
'Codex preflight assertion returned a different receipt.',
|
|
110
|
+
{
|
|
111
|
+
code: 'CODEX_PREFLIGHT_INVALID',
|
|
112
|
+
runtime: 'codex',
|
|
113
|
+
availabilityState: 'available',
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const profile = asserted?.expectedStaticProfile;
|
|
118
|
+
const spawnProfileId = requireConfigString(
|
|
119
|
+
asserted?.spawnProfileId,
|
|
120
|
+
'spawn profile ID',
|
|
121
|
+
'codexAvailability',
|
|
122
|
+
);
|
|
123
|
+
for (const [field, label] of [
|
|
124
|
+
['model', 'Codex model'],
|
|
125
|
+
['effort', 'Codex effort'],
|
|
126
|
+
['cwd', 'Codex cwd'],
|
|
127
|
+
]) {
|
|
128
|
+
requireConfigString(profile?.[field], label, 'codexAvailability');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const availability = Object.freeze({
|
|
132
|
+
state: 'available',
|
|
133
|
+
spawnProfileId,
|
|
134
|
+
});
|
|
135
|
+
assertedReceiptByAvailability.set(availability, asserted);
|
|
136
|
+
return availability;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {object} opts
|
|
141
|
+
* @param {object} [opts.config]
|
|
142
|
+
* @param {string|number|null} [opts.chatId]
|
|
143
|
+
* @param {string|number|null} [opts.threadId]
|
|
144
|
+
* @param {string} [opts.defaultPm='sdk']
|
|
145
|
+
* @param {object} [opts.codexAvailability]
|
|
146
|
+
* @param {object|null} [opts.composedClaudeOptions]
|
|
147
|
+
* The final result of Polygram's buildSdkOptions composition. Without it,
|
|
148
|
+
* model/effort remain unknown rather than inventing precedence.
|
|
149
|
+
* @param {object} [opts.logger]
|
|
150
|
+
* @returns {object}
|
|
151
|
+
*/
|
|
152
|
+
function resolveRuntimeConfig({
|
|
153
|
+
config = {},
|
|
154
|
+
chatId = null,
|
|
155
|
+
threadId = null,
|
|
156
|
+
defaultPm = 'sdk',
|
|
157
|
+
codexAvailability,
|
|
158
|
+
composedClaudeOptions = null,
|
|
159
|
+
logger = console,
|
|
160
|
+
} = {}) {
|
|
161
|
+
const scopes = configScopes(config, chatId, threadId);
|
|
162
|
+
const selection = resolveRuntimeDescriptor({
|
|
163
|
+
config,
|
|
164
|
+
chatId,
|
|
165
|
+
threadId,
|
|
166
|
+
defaultPm,
|
|
167
|
+
logger,
|
|
168
|
+
});
|
|
169
|
+
const pmChoice = {
|
|
170
|
+
value: selection.configuredPm,
|
|
171
|
+
source: selection.source,
|
|
172
|
+
};
|
|
173
|
+
const isCodex = selection.runtime === 'codex';
|
|
174
|
+
const descriptor = selection.descriptor;
|
|
175
|
+
|
|
176
|
+
let modelChoice;
|
|
177
|
+
let effortChoice;
|
|
178
|
+
let cwdChoice;
|
|
179
|
+
let agentChoice;
|
|
180
|
+
let spawnProfileId = null;
|
|
181
|
+
|
|
182
|
+
if (isCodex) {
|
|
183
|
+
const receipt = validateCodexAvailability(codexAvailability);
|
|
184
|
+
modelChoice = resolveScopedValue(
|
|
185
|
+
scopes.providerOptions,
|
|
186
|
+
'codexModel',
|
|
187
|
+
null,
|
|
188
|
+
);
|
|
189
|
+
effortChoice = resolveScopedValue(
|
|
190
|
+
scopes.providerOptions,
|
|
191
|
+
'codexEffort',
|
|
192
|
+
null,
|
|
193
|
+
);
|
|
194
|
+
cwdChoice = resolveScopedValue(scopes.codexSpawn, 'cwd', null);
|
|
195
|
+
agentChoice = { value: null, source: null };
|
|
196
|
+
|
|
197
|
+
modelChoice.value = requireConfigString(
|
|
198
|
+
modelChoice.value,
|
|
199
|
+
'Codex model',
|
|
200
|
+
modelChoice.source,
|
|
201
|
+
'CODEX_RUNTIME_SELECTION_INCOMPLETE',
|
|
202
|
+
);
|
|
203
|
+
effortChoice.value = requireConfigString(
|
|
204
|
+
effortChoice.value,
|
|
205
|
+
'Codex effort',
|
|
206
|
+
effortChoice.source,
|
|
207
|
+
'CODEX_RUNTIME_SELECTION_INCOMPLETE',
|
|
208
|
+
);
|
|
209
|
+
cwdChoice.value = requireConfigString(
|
|
210
|
+
cwdChoice.value,
|
|
211
|
+
'Codex cwd',
|
|
212
|
+
cwdChoice.source,
|
|
213
|
+
'CODEX_RUNTIME_SELECTION_INCOMPLETE',
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const expected = receipt.expectedStaticProfile;
|
|
217
|
+
if (expected.cwd !== cwdChoice.value) {
|
|
218
|
+
throw new RuntimeConfigError(
|
|
219
|
+
'Codex preflight profile does not match selected cwd.',
|
|
220
|
+
{
|
|
221
|
+
code: 'CODEX_PREFLIGHT_PROFILE_MISMATCH',
|
|
222
|
+
runtime: 'codex',
|
|
223
|
+
availabilityState: 'available',
|
|
224
|
+
reason: 'mismatched fields: cwd',
|
|
225
|
+
action: 'Run preflight with the exact selected cwd.',
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const selectedModels = Array.isArray(receipt.modelCatalog)
|
|
230
|
+
? receipt.modelCatalog.filter(
|
|
231
|
+
(entry) => entry?.model === modelChoice.value,
|
|
232
|
+
)
|
|
233
|
+
: [];
|
|
234
|
+
if (
|
|
235
|
+
selectedModels.length !== 1
|
|
236
|
+
|| !Array.isArray(selectedModels[0].supportedReasoningEfforts)
|
|
237
|
+
|| !selectedModels[0].supportedReasoningEfforts
|
|
238
|
+
.includes(effortChoice.value)
|
|
239
|
+
) {
|
|
240
|
+
throw new RuntimeConfigError(
|
|
241
|
+
'Codex model and effort are not in the authenticated catalog.',
|
|
242
|
+
{
|
|
243
|
+
code: 'CODEX_PREFLIGHT_PROFILE_MISMATCH',
|
|
244
|
+
runtime: 'codex',
|
|
245
|
+
availabilityState: 'available',
|
|
246
|
+
reason: 'selected model/effort pair is unavailable',
|
|
247
|
+
action: 'Choose a model and effort from the authenticated Codex catalog.',
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
spawnProfileId = receipt.spawnProfileId;
|
|
252
|
+
} else {
|
|
253
|
+
const chat = scopes.chat;
|
|
254
|
+
const topic = scopes.topic;
|
|
255
|
+
cwdChoice = resolveTruthyValue([
|
|
256
|
+
['topic', topic],
|
|
257
|
+
['chat', chat],
|
|
258
|
+
], 'cwd');
|
|
259
|
+
agentChoice = resolveTruthyValue([
|
|
260
|
+
['topic', topic],
|
|
261
|
+
['chat', chat],
|
|
262
|
+
], 'agent');
|
|
263
|
+
cwdChoice.value = optionalConfigString(
|
|
264
|
+
cwdChoice.value,
|
|
265
|
+
'Claude cwd',
|
|
266
|
+
cwdChoice.source,
|
|
267
|
+
);
|
|
268
|
+
agentChoice.value = optionalConfigString(
|
|
269
|
+
agentChoice.value,
|
|
270
|
+
'Claude agent',
|
|
271
|
+
agentChoice.source,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (composedClaudeOptions == null) {
|
|
275
|
+
modelChoice = { value: null, source: null };
|
|
276
|
+
effortChoice = { value: null, source: null };
|
|
277
|
+
} else {
|
|
278
|
+
if (
|
|
279
|
+
typeof composedClaudeOptions !== 'object'
|
|
280
|
+
|| Array.isArray(composedClaudeOptions)
|
|
281
|
+
) {
|
|
282
|
+
throw invalidConfigValue(
|
|
283
|
+
'composed Claude options',
|
|
284
|
+
composedClaudeOptions,
|
|
285
|
+
'composed-options',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
modelChoice = {
|
|
289
|
+
value: optionalConfigString(
|
|
290
|
+
composedClaudeOptions.model,
|
|
291
|
+
'composed Claude model',
|
|
292
|
+
'composed-options',
|
|
293
|
+
),
|
|
294
|
+
source: 'composed-options',
|
|
295
|
+
};
|
|
296
|
+
effortChoice = {
|
|
297
|
+
value: optionalConfigString(
|
|
298
|
+
composedClaudeOptions.effort,
|
|
299
|
+
'composed Claude effort',
|
|
300
|
+
'composed-options',
|
|
301
|
+
),
|
|
302
|
+
source: 'composed-options',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const modelFamily = descriptor.provider;
|
|
308
|
+
const identity = stableRuntimeConfigIdentity({
|
|
309
|
+
runtime: descriptor.runtime,
|
|
310
|
+
backend: descriptor.backend,
|
|
311
|
+
sessionNamespace: descriptor.sessionNamespace,
|
|
312
|
+
promptMode: descriptor.promptMode,
|
|
313
|
+
modelFamily,
|
|
314
|
+
model: modelChoice.value,
|
|
315
|
+
effort: effortChoice.value,
|
|
316
|
+
cwd: cwdChoice.value,
|
|
317
|
+
agent: agentChoice.value,
|
|
318
|
+
spawnProfileId,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return Object.freeze({
|
|
322
|
+
configuredPm: pmChoice.value,
|
|
323
|
+
...descriptor,
|
|
324
|
+
modelFamily,
|
|
325
|
+
modelField: modelFamily === 'codex' ? 'codexModel' : 'model',
|
|
326
|
+
effortField: modelFamily === 'codex' ? 'codexEffort' : 'effort',
|
|
327
|
+
model: modelChoice.value,
|
|
328
|
+
effort: effortChoice.value,
|
|
329
|
+
cwd: cwdChoice.value,
|
|
330
|
+
agent: agentChoice.value,
|
|
331
|
+
sources: Object.freeze({
|
|
332
|
+
pm: pmChoice.source,
|
|
333
|
+
model: modelChoice.source,
|
|
334
|
+
effort: effortChoice.source,
|
|
335
|
+
cwd: cwdChoice.source,
|
|
336
|
+
agent: agentChoice.source,
|
|
337
|
+
}),
|
|
338
|
+
spawnProfileId,
|
|
339
|
+
// This digest belongs to Polygram's config/application layer. Orchestra's
|
|
340
|
+
// strict replaceRuntime uses its own runtime/backend/profile identity.
|
|
341
|
+
runtimeConfigIdentity: identity,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve only the operator-selected Codex inputs needed to run the matching
|
|
347
|
+
* preflight. This does not make Codex available and carries no receipt.
|
|
348
|
+
*/
|
|
349
|
+
function resolveCodexRuntimeRequest({
|
|
350
|
+
config = {},
|
|
351
|
+
chatId = null,
|
|
352
|
+
threadId = null,
|
|
353
|
+
defaultPm = 'sdk',
|
|
354
|
+
} = {}) {
|
|
355
|
+
const pmChoice = resolvePmChoice(config, chatId, threadId, defaultPm);
|
|
356
|
+
if (!chatId || pmChoice.value !== 'codex') return null;
|
|
357
|
+
|
|
358
|
+
const scopes = configScopes(config, chatId, threadId);
|
|
359
|
+
const modelChoice = resolveScopedValue(
|
|
360
|
+
scopes.providerOptions,
|
|
361
|
+
'codexModel',
|
|
362
|
+
null,
|
|
363
|
+
);
|
|
364
|
+
const effortChoice = resolveScopedValue(
|
|
365
|
+
scopes.providerOptions,
|
|
366
|
+
'codexEffort',
|
|
367
|
+
null,
|
|
368
|
+
);
|
|
369
|
+
const cwdChoice = resolveScopedValue(scopes.codexSpawn, 'cwd', null);
|
|
370
|
+
const sources = Object.freeze({
|
|
371
|
+
pm: pmChoice.source,
|
|
372
|
+
model: modelChoice.source,
|
|
373
|
+
effort: effortChoice.source,
|
|
374
|
+
cwd: cwdChoice.source,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return Object.freeze({
|
|
378
|
+
runtime: 'codex',
|
|
379
|
+
model: requireConfigString(
|
|
380
|
+
modelChoice.value,
|
|
381
|
+
'Codex model',
|
|
382
|
+
modelChoice.source,
|
|
383
|
+
'CODEX_RUNTIME_SELECTION_INCOMPLETE',
|
|
384
|
+
),
|
|
385
|
+
effort: requireConfigString(
|
|
386
|
+
effortChoice.value,
|
|
387
|
+
'Codex effort',
|
|
388
|
+
effortChoice.source,
|
|
389
|
+
'CODEX_RUNTIME_SELECTION_INCOMPLETE',
|
|
390
|
+
),
|
|
391
|
+
cwd: requireConfigString(
|
|
392
|
+
cwdChoice.value,
|
|
393
|
+
'Codex cwd',
|
|
394
|
+
cwdChoice.source,
|
|
395
|
+
'CODEX_RUNTIME_SELECTION_INCOMPLETE',
|
|
396
|
+
),
|
|
397
|
+
sources,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function configScopes(config, chatId, threadId) {
|
|
402
|
+
const hasChat = Boolean(chatId);
|
|
403
|
+
const chat = hasChat ? config?.chats?.[chatId] : null;
|
|
404
|
+
const topic = hasChat && threadId ? chat?.topics?.[threadId] : null;
|
|
405
|
+
return {
|
|
406
|
+
chat,
|
|
407
|
+
topic,
|
|
408
|
+
providerOptions: [
|
|
409
|
+
['topic', topic],
|
|
410
|
+
['chat', chat],
|
|
411
|
+
['bot', config?.bot],
|
|
412
|
+
['default', config?.defaults],
|
|
413
|
+
],
|
|
414
|
+
codexSpawn: [
|
|
415
|
+
['topic', topic],
|
|
416
|
+
['chat', chat],
|
|
417
|
+
['default', config?.defaults],
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function resolvePmChoice(config, chatId, threadId, defaultPm) {
|
|
423
|
+
if (!chatId) return { value: defaultPm, source: 'default' };
|
|
424
|
+
const chat = config?.chats?.[chatId];
|
|
425
|
+
const topic = threadId && chat?.topics?.[threadId];
|
|
426
|
+
if (topic?.pm) return { value: topic.pm, source: 'topic' };
|
|
427
|
+
if (chat?.pm) return { value: chat.pm, source: 'chat' };
|
|
428
|
+
if (config?.bot?.pm) return { value: config.bot.pm, source: 'bot' };
|
|
429
|
+
return { value: defaultPm, source: 'default' };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function resolveRuntimeDescriptor({
|
|
433
|
+
config = {},
|
|
434
|
+
chatId = null,
|
|
435
|
+
threadId = null,
|
|
436
|
+
defaultPm = 'sdk',
|
|
437
|
+
logger = console,
|
|
438
|
+
} = {}) {
|
|
439
|
+
const pmChoice = resolvePmChoice(config, chatId, threadId, defaultPm);
|
|
440
|
+
const isCodex = Boolean(chatId) && pmChoice.value === 'codex';
|
|
441
|
+
const pickedBackend = isCodex
|
|
442
|
+
? 'codex'
|
|
443
|
+
: pickBackend({
|
|
444
|
+
config,
|
|
445
|
+
chatId,
|
|
446
|
+
threadId,
|
|
447
|
+
logger,
|
|
448
|
+
pmDefault: defaultPm,
|
|
449
|
+
});
|
|
450
|
+
// Orchestra's factory treats every non-CLI result as SDK. In particular,
|
|
451
|
+
// pickBackend returns pmDefault untouched when chatId is falsy.
|
|
452
|
+
const canonicalBackend = isCodex
|
|
453
|
+
? 'codex'
|
|
454
|
+
: pickedBackend === 'cli'
|
|
455
|
+
? 'cli'
|
|
456
|
+
: 'sdk';
|
|
457
|
+
return Object.freeze({
|
|
458
|
+
configuredPm: pmChoice.value,
|
|
459
|
+
source: pmChoice.source,
|
|
460
|
+
runtime: PM_DESCRIPTORS[canonicalBackend].runtime,
|
|
461
|
+
backend: canonicalBackend,
|
|
462
|
+
promptMode: PM_DESCRIPTORS[canonicalBackend].promptMode,
|
|
463
|
+
descriptor: PM_DESCRIPTORS[canonicalBackend],
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function resolveScopedValue(scopes, field, fallback) {
|
|
468
|
+
for (const [source, scope] of scopes) {
|
|
469
|
+
if (
|
|
470
|
+
scope
|
|
471
|
+
&& Object.hasOwn(scope, field)
|
|
472
|
+
&& scope[field] !== null
|
|
473
|
+
&& scope[field] !== undefined
|
|
474
|
+
) {
|
|
475
|
+
return { value: scope[field], source };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return { value: fallback, source: 'default' };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function resolveTruthyValue(scopes, field) {
|
|
482
|
+
for (const [source, scope] of scopes) {
|
|
483
|
+
if (scope?.[field]) return { value: scope[field], source };
|
|
484
|
+
}
|
|
485
|
+
return { value: null, source: null };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function validateCodexAvailability(input) {
|
|
489
|
+
const availability = input ?? DEFAULT_CODEX_AVAILABILITY;
|
|
490
|
+
if (availability?.state === 'available') {
|
|
491
|
+
const receipt = (
|
|
492
|
+
availability
|
|
493
|
+
&& typeof availability === 'object'
|
|
494
|
+
&& assertedReceiptByAvailability.get(availability)
|
|
495
|
+
);
|
|
496
|
+
if (!receipt) {
|
|
497
|
+
throw new RuntimeConfigError(
|
|
498
|
+
'Codex availability was not constructed from an asserted spawn receipt.',
|
|
499
|
+
{
|
|
500
|
+
code: 'CODEX_PREFLIGHT_INVALID',
|
|
501
|
+
runtime: 'codex',
|
|
502
|
+
availabilityState: 'available',
|
|
503
|
+
reason: 'unbranded or copied availability object',
|
|
504
|
+
action: 'Use createCodexRuntimeAvailability with the exact preflight receipt.',
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return receipt;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const errorInfo = CODEX_ERROR_BY_STATE[availability?.state];
|
|
512
|
+
if (!errorInfo) {
|
|
513
|
+
throw new RuntimeConfigError(
|
|
514
|
+
`Unknown Codex availability state '${String(availability?.state)}'`,
|
|
515
|
+
{
|
|
516
|
+
code: 'RUNTIME_CONFIG_INVALID',
|
|
517
|
+
source: 'codexAvailability',
|
|
518
|
+
value: availability?.state,
|
|
519
|
+
runtime: 'codex',
|
|
520
|
+
action: 'Use available, loading, unavailable, or ineligible.',
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const reason = typeof availability.reason === 'string' && availability.reason
|
|
526
|
+
? availability.reason
|
|
527
|
+
: 'No additional reason was provided';
|
|
528
|
+
throw new RuntimeConfigError(
|
|
529
|
+
`Codex runtime is ${availability.state}: ${reason}. ${errorInfo.action}`,
|
|
530
|
+
{
|
|
531
|
+
code: errorInfo.code,
|
|
532
|
+
runtime: 'codex',
|
|
533
|
+
availabilityState: availability.state,
|
|
534
|
+
reason,
|
|
535
|
+
action: errorInfo.action,
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function invalidConfigValue(label, value, source, code = 'RUNTIME_CONFIG_VALUE_INVALID') {
|
|
541
|
+
return new RuntimeConfigError(
|
|
542
|
+
`${label} at ${source ?? 'unknown'} scope must be a bounded string.`,
|
|
543
|
+
{
|
|
544
|
+
code,
|
|
545
|
+
source,
|
|
546
|
+
value: (
|
|
547
|
+
typeof value === 'string'
|
|
548
|
+
|| typeof value === 'number'
|
|
549
|
+
|| typeof value === 'boolean'
|
|
550
|
+
) ? value : null,
|
|
551
|
+
runtime: label.startsWith('Codex') ? 'codex' : null,
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function requireConfigString(
|
|
557
|
+
value,
|
|
558
|
+
label,
|
|
559
|
+
source,
|
|
560
|
+
missingCode = 'RUNTIME_CONFIG_VALUE_INVALID',
|
|
561
|
+
) {
|
|
562
|
+
if (
|
|
563
|
+
typeof value !== 'string'
|
|
564
|
+
|| value.trim().length === 0
|
|
565
|
+
|| value.length > MAX_CONFIG_VALUE_LENGTH
|
|
566
|
+
) {
|
|
567
|
+
const code = value == null || value === ''
|
|
568
|
+
? missingCode
|
|
569
|
+
: 'RUNTIME_CONFIG_VALUE_INVALID';
|
|
570
|
+
throw invalidConfigValue(label, value, source, code);
|
|
571
|
+
}
|
|
572
|
+
return value;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function optionalConfigString(value, label, source) {
|
|
576
|
+
if (value == null) return null;
|
|
577
|
+
return requireConfigString(value, label, source);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function stableRuntimeConfigIdentity(fields) {
|
|
581
|
+
const model = fields.runtime === 'codex' ? null : fields.model;
|
|
582
|
+
const effort = fields.runtime === 'codex' ? null : fields.effort;
|
|
583
|
+
const canonical = JSON.stringify([
|
|
584
|
+
fields.runtime,
|
|
585
|
+
fields.backend,
|
|
586
|
+
fields.sessionNamespace,
|
|
587
|
+
fields.promptMode,
|
|
588
|
+
fields.modelFamily,
|
|
589
|
+
model,
|
|
590
|
+
effort,
|
|
591
|
+
fields.cwd,
|
|
592
|
+
fields.agent,
|
|
593
|
+
fields.spawnProfileId,
|
|
594
|
+
]);
|
|
595
|
+
const digest = createHash('sha256').update(canonical).digest('hex');
|
|
596
|
+
return `runtime-config:v1:${digest}`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
module.exports = {
|
|
600
|
+
createCodexRuntimeAvailability,
|
|
601
|
+
RuntimeConfigError,
|
|
602
|
+
resolveCodexRuntimeRequest,
|
|
603
|
+
resolveRuntimeConfig,
|
|
604
|
+
resolveRuntimeDescriptor,
|
|
605
|
+
};
|
package/lib/sdk/build-options.js
CHANGED
|
@@ -110,9 +110,12 @@ function createBuildSdkOptions({
|
|
|
110
110
|
// was unconditionally exported so the deleted bin/approval-hook.js
|
|
111
111
|
// could authenticate; with the hook gone, the only IPC consumers
|
|
112
112
|
// are external scripts (cron-driven sends) running in their own
|
|
113
|
-
// processes with
|
|
113
|
+
// processes with access to the owner-only IPC runtime directory.
|
|
114
114
|
if (botConfig.exposeIpcSecretToChildren && processEnv.POLYGRAM_IPC_SECRET) {
|
|
115
115
|
childEnv.POLYGRAM_IPC_SECRET = processEnv.POLYGRAM_IPC_SECRET;
|
|
116
|
+
if (processEnv.POLYGRAM_IPC_DIR) {
|
|
117
|
+
childEnv.POLYGRAM_IPC_DIR = processEnv.POLYGRAM_IPC_DIR;
|
|
118
|
+
}
|
|
116
119
|
}
|
|
117
120
|
if (botConfig.needsToken) {
|
|
118
121
|
childEnv.TELEGRAM_BOT_TOKEN = botConfig.token || '';
|
package/lib/sdk/callbacks.js
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
|
|
23
23
|
const { getTopicConfig } = require('../session-key');
|
|
24
24
|
const { pickBackend } = require('@shumkov/orchestra');
|
|
25
|
+
const {
|
|
26
|
+
isCodexProcess,
|
|
27
|
+
normalizeStreamText,
|
|
28
|
+
} = require('../codex/event-normalizer');
|
|
25
29
|
|
|
26
30
|
function createSdkCallbacks({
|
|
27
31
|
db,
|
|
@@ -91,6 +95,22 @@ function createSdkCallbacks({
|
|
|
91
95
|
// making historical telemetry meaningless.
|
|
92
96
|
const chatConfig = config.chats[entry.chatId] || {};
|
|
93
97
|
const topicConfig = getTopicConfig(chatConfig, entry.threadId || null);
|
|
98
|
+
if (isCodexProcess(entry)) {
|
|
99
|
+
dbWrite(() => db.upsertProviderSession({
|
|
100
|
+
session_key: sessionKey,
|
|
101
|
+
namespace: 'codex:app-server',
|
|
102
|
+
provider: 'codex',
|
|
103
|
+
provider_session_id: event.providerSessionId ?? event.session_id,
|
|
104
|
+
app_server_session_id: event.appServerSessionId ?? null,
|
|
105
|
+
agent: null,
|
|
106
|
+
cwd: entry.cwd,
|
|
107
|
+
model: entry.model,
|
|
108
|
+
effort: entry.effort,
|
|
109
|
+
pm_backend: 'codex',
|
|
110
|
+
}), `upsert Codex session ${sessionKey}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
dbWrite(() => db.upsertSession({
|
|
95
115
|
session_key: sessionKey,
|
|
96
116
|
chat_id: entry.chatId,
|
|
@@ -107,9 +127,22 @@ function createSdkCallbacks({
|
|
|
107
127
|
}), `upsert session ${sessionKey}`);
|
|
108
128
|
},
|
|
109
129
|
|
|
110
|
-
onClose: (sessionKey, code,
|
|
130
|
+
onClose: (sessionKey, code, closeDetailOrEntry, appendedEntry) => {
|
|
131
|
+
// ProcessManager appends the live Process after the event arguments.
|
|
132
|
+
// Claude emits only `code`, while Codex emits `code, closeDetail`; keep
|
|
133
|
+
// the detail from shifting the process entry out of this callback.
|
|
134
|
+
const entry = appendedEntry ?? closeDetailOrEntry;
|
|
135
|
+
const closeDetail = appendedEntry ? closeDetailOrEntry : null;
|
|
111
136
|
logger.log?.(`[${entry.label}] Process exited (code ${code})`);
|
|
112
|
-
logEvent('process-close', {
|
|
137
|
+
logEvent('process-close', {
|
|
138
|
+
chat_id: entry.chatId,
|
|
139
|
+
session_key: sessionKey,
|
|
140
|
+
code,
|
|
141
|
+
...(closeDetail?.generationId
|
|
142
|
+
? { generation_id: closeDetail.generationId }
|
|
143
|
+
: {}),
|
|
144
|
+
...(closeDetail?.reason ? { reason: closeDetail.reason } : {}),
|
|
145
|
+
});
|
|
113
146
|
// 0.13 D3: a session closing mid-autonomous-cycle must tear down the
|
|
114
147
|
// controller's visuals (typing loop + anchor reaction) — the safety net
|
|
115
148
|
// against a forever-typing leak on a dead session.
|
|
@@ -128,12 +161,22 @@ function createSdkCallbacks({
|
|
|
128
161
|
},
|
|
129
162
|
|
|
130
163
|
onStreamChunk: (sessionKey, partial, entry) => {
|
|
164
|
+
const normalized = normalizeStreamText(partial, entry);
|
|
165
|
+
if (isCodexProcess(entry) && normalized == null) {
|
|
166
|
+
logEvent('codex-stream-event-invalid', {
|
|
167
|
+
chat_id: entry?.chatId ?? getChatIdFromKey(sessionKey),
|
|
168
|
+
session_key: sessionKey,
|
|
169
|
+
generation_id: entry?.generationId ?? null,
|
|
170
|
+
value_type: typeof partial,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
131
174
|
// Route to the head pending's per-turn streamer. In the
|
|
132
175
|
// concurrent-pending model, only the HEAD is the turn Claude
|
|
133
176
|
// is actively emitting events for.
|
|
134
177
|
const head = entry.pendingQueue?.[0];
|
|
135
178
|
const s = head?.context?.streamer;
|
|
136
|
-
if (s) s.onChunk(
|
|
179
|
+
if (s) s.onChunk(normalized).catch(() => {});
|
|
137
180
|
// Heartbeat the reactor so long text generation doesn't trip
|
|
138
181
|
// the 10s STALL → 🥱 / 30s TIMEOUT → 😨 promotion.
|
|
139
182
|
const r = head?.context?.reactor;
|