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.
Files changed (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +5 -3
  3. package/config.example.json +23 -1
  4. package/lib/codex/binary.js +287 -0
  5. package/lib/codex/delivery-finalizer.js +97 -0
  6. package/lib/codex/diagnostics.js +483 -0
  7. package/lib/codex/event-normalizer.js +50 -0
  8. package/lib/codex/host-identity.js +103 -0
  9. package/lib/codex/model-catalog.js +359 -0
  10. package/lib/codex/retention-scheduler.js +59 -0
  11. package/lib/codex/runtime-controller.js +1292 -0
  12. package/lib/codex/runtime-profile.js +888 -0
  13. package/lib/codex/spawn-context.js +87 -0
  14. package/lib/db/auto-resume.js +77 -9
  15. package/lib/db/codex-reconciliation.js +490 -0
  16. package/lib/db/codex-retention.js +139 -0
  17. package/lib/db/sessions.js +102 -1
  18. package/lib/db.js +3418 -19
  19. package/lib/error/classify.js +140 -0
  20. package/lib/handlers/abort.js +143 -5
  21. package/lib/handlers/autosteer.js +256 -1
  22. package/lib/handlers/codex-reconciliation.js +230 -0
  23. package/lib/handlers/config-callback.js +324 -89
  24. package/lib/handlers/config-ui.js +401 -39
  25. package/lib/handlers/dispatcher.js +71 -8
  26. package/lib/handlers/edit-correction.js +11 -1
  27. package/lib/handlers/edit-redelivery.js +230 -4
  28. package/lib/handlers/gate-inbound.js +7 -0
  29. package/lib/handlers/replay-disposition.js +133 -11
  30. package/lib/handlers/slash-commands.js +443 -37
  31. package/lib/ipc/client.js +22 -12
  32. package/lib/ipc/runtime-directory.js +208 -0
  33. package/lib/ipc/server.js +18 -12
  34. package/lib/model-costs.js +4 -3
  35. package/lib/pm-interface.js +33 -0
  36. package/lib/prompt.js +15 -9
  37. package/lib/rewind/rewind.js +21 -1
  38. package/lib/runtime-config.js +605 -0
  39. package/lib/sdk/build-options.js +4 -1
  40. package/lib/sdk/callbacks.js +46 -3
  41. package/migrations/015-agent-runtime-sessions.sql +50 -0
  42. package/migrations/016-agent-turn-attempts.sql +211 -0
  43. package/migrations/017-config-runtime-changes.sql +46 -0
  44. package/ops/README.md +57 -0
  45. package/package.json +2 -2
  46. package/polygram.js +697 -48
  47. package/scripts/doctor.js +37 -8
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const DAY_MS = 86_400_000;
4
+ const SETTLED_RETENTION_MS = 30 * DAY_MS;
5
+ const AMBIGUOUS_RETENTION_MS = 90 * DAY_MS;
6
+
7
+ function pruneCodexOperationalData(rawDb, {
8
+ now = Date.now(),
9
+ settledRetentionMs = SETTLED_RETENTION_MS,
10
+ ambiguousRetentionMs = AMBIGUOUS_RETENTION_MS,
11
+ } = {}) {
12
+ for (const [value, label] of [
13
+ [now, 'now'],
14
+ [settledRetentionMs, 'settledRetentionMs'],
15
+ [ambiguousRetentionMs, 'ambiguousRetentionMs'],
16
+ ]) {
17
+ if (!Number.isSafeInteger(value) || value < 0) {
18
+ throw new TypeError(`Codex retention ${label} must be a non-negative integer`);
19
+ }
20
+ }
21
+ if (ambiguousRetentionMs < settledRetentionMs) {
22
+ throw new TypeError(
23
+ 'Codex ambiguous retention cannot be shorter than settled retention',
24
+ );
25
+ }
26
+
27
+ return rawDb.transaction(() => {
28
+ const settledCutoff = now - settledRetentionMs;
29
+ const ambiguousCutoff = now - ambiguousRetentionMs;
30
+ const attempts = rawDb.prepare(`
31
+ DELETE FROM codex_turn_attempts
32
+ WHERE (
33
+ (
34
+ recovery_state = 'settled'
35
+ AND settled_ts IS NOT NULL
36
+ AND settled_ts < @settledCutoff
37
+ )
38
+ OR
39
+ (
40
+ recovery_state = 'ambiguous'
41
+ AND ambiguous_ts IS NOT NULL
42
+ AND ambiguous_ts < @ambiguousCutoff
43
+ AND EXISTS (
44
+ SELECT 1
45
+ FROM codex_attempt_reconciliations r
46
+ WHERE r.attempt_id = codex_turn_attempts.attempt_id
47
+ )
48
+ )
49
+ )
50
+ AND NOT EXISTS (
51
+ SELECT 1
52
+ FROM codex_daemon_lease lease
53
+ WHERE lease.generation_id = codex_turn_attempts.generation_id
54
+ AND lease.status IN ('active', 'quarantined')
55
+ )
56
+ AND NOT EXISTS (
57
+ SELECT 1
58
+ FROM codex_linked_inputs linked
59
+ WHERE (
60
+ linked.attempt_id = codex_turn_attempts.attempt_id
61
+ OR linked.target_attempt_id = codex_turn_attempts.attempt_id
62
+ )
63
+ AND linked.state = 'linked'
64
+ )
65
+ AND NOT EXISTS (
66
+ SELECT 1
67
+ FROM codex_retry_reservations retry
68
+ WHERE (
69
+ retry.original_attempt_id = codex_turn_attempts.attempt_id
70
+ OR retry.retry_attempt_id = codex_turn_attempts.attempt_id
71
+ )
72
+ AND retry.state != 'retired'
73
+ )
74
+ AND NOT EXISTS (
75
+ SELECT 1
76
+ FROM codex_generations generation
77
+ WHERE generation.generation_id = codex_turn_attempts.generation_id
78
+ AND generation.state = 'containment-failed'
79
+ AND NOT EXISTS (
80
+ SELECT 1
81
+ FROM codex_reboot_releases release
82
+ WHERE release.stable_host_id = generation.stable_host_id
83
+ AND release.incident_boot_session_id =
84
+ generation.boot_session_id
85
+ )
86
+ )
87
+ `).run({ settledCutoff, ambiguousCutoff });
88
+
89
+ const generations = rawDb.prepare(`
90
+ DELETE FROM codex_generations
91
+ WHERE (
92
+ (
93
+ state IN ('healthy-stopped', 'retired')
94
+ AND settled_ts IS NOT NULL
95
+ AND settled_ts < @settledCutoff
96
+ )
97
+ OR
98
+ (
99
+ state = 'created'
100
+ AND updated_ts < @ambiguousCutoff
101
+ )
102
+ OR
103
+ (
104
+ state = 'containment-failed'
105
+ AND updated_ts < @ambiguousCutoff
106
+ AND EXISTS (
107
+ SELECT 1
108
+ FROM codex_reboot_releases release
109
+ WHERE release.stable_host_id = codex_generations.stable_host_id
110
+ AND release.incident_boot_session_id =
111
+ codex_generations.boot_session_id
112
+ )
113
+ )
114
+ )
115
+ AND NOT EXISTS (
116
+ SELECT 1
117
+ FROM codex_turn_attempts attempt
118
+ WHERE attempt.generation_id = codex_generations.generation_id
119
+ )
120
+ AND NOT EXISTS (
121
+ SELECT 1
122
+ FROM codex_daemon_lease lease
123
+ WHERE lease.generation_id = codex_generations.generation_id
124
+ AND lease.status IN ('active', 'quarantined')
125
+ )
126
+ `).run({ settledCutoff, ambiguousCutoff });
127
+ return {
128
+ deletedAttempts: attempts.changes,
129
+ deletedGenerations: generations.changes,
130
+ };
131
+ })();
132
+ }
133
+
134
+ module.exports = {
135
+ DAY_MS,
136
+ SETTLED_RETENTION_MS,
137
+ AMBIGUOUS_RETENTION_MS,
138
+ pruneCodexOperationalData,
139
+ };
@@ -11,6 +11,26 @@ const path = require('path');
11
11
 
12
12
  function now() { return Date.now(); }
13
13
 
14
+ const CLAUDE_INLINE_NAMESPACE = 'claude:inline';
15
+ const CLAUDE_CHANNELS_NAMESPACE = 'claude:channels';
16
+ const CODEX_APP_SERVER_NAMESPACE = 'codex:app-server';
17
+ const CHANNELS_CLASS = new Set(['channels', 'cli']);
18
+
19
+ /**
20
+ * Resolve the compatibility namespace from canonical runtime/backend identity.
21
+ * Historical stored `tmux` rows are inline. Current config `pm: "tmux"` is
22
+ * normalized to canonical `cli` before it reaches this resolver.
23
+ */
24
+ function sessionNamespaceFor({ runtime = 'claude', backend = null } = {}) {
25
+ if (runtime === 'codex') return CODEX_APP_SERVER_NAMESPACE;
26
+ if (runtime !== 'claude') {
27
+ throw new TypeError(`unsupported provider runtime: ${runtime}`);
28
+ }
29
+ return CHANNELS_CLASS.has(backend)
30
+ ? CLAUDE_CHANNELS_NAMESPACE
31
+ : CLAUDE_INLINE_NAMESPACE;
32
+ }
33
+
14
34
  function countSessions(db) {
15
35
  return db.raw.prepare('SELECT COUNT(*) AS c FROM sessions').get().c;
16
36
  }
@@ -95,6 +115,78 @@ function getClaudeSessionId(db, sessionKey) {
95
115
  return row?.claude_session_id || null;
96
116
  }
97
117
 
118
+ function resolveProviderSessionForSpawn(db, sessionKey, resolved = {}) {
119
+ const namespace = sessionNamespaceFor(resolved);
120
+ if (!db) {
121
+ return {
122
+ existingSessionId: null,
123
+ appServerSessionId: null,
124
+ namespace,
125
+ drift: null,
126
+ };
127
+ }
128
+ if (resolved.runtime !== 'codex') {
129
+ const legacy = resolveSessionForSpawn(db, sessionKey, resolved);
130
+ const row = legacy.existingSessionId
131
+ ? db.getProviderSession(sessionKey, namespace)
132
+ : null;
133
+ return {
134
+ ...legacy,
135
+ appServerSessionId: null,
136
+ namespace,
137
+ providerSession: row ?? null,
138
+ };
139
+ }
140
+
141
+ const row = db.getProviderSession(sessionKey, namespace);
142
+ if (!row) {
143
+ return {
144
+ existingSessionId: null,
145
+ appServerSessionId: null,
146
+ namespace,
147
+ drift: null,
148
+ };
149
+ }
150
+ const before = {
151
+ agent: row.agent || null,
152
+ cwd: row.cwd || null,
153
+ model: row.model || null,
154
+ effort: row.effort || null,
155
+ };
156
+ const after = {
157
+ agent: resolved.agent || null,
158
+ cwd: resolved.cwd || null,
159
+ model: resolved.model || null,
160
+ effort: resolved.effort || null,
161
+ };
162
+ const fields = CODEX_SPAWN_IDENTITY_FIELDS.filter((field) => (
163
+ after[field] != null && after[field] !== before[field]
164
+ ));
165
+ if (fields.length > 0) {
166
+ db.clearProviderSession(sessionKey, namespace);
167
+ return {
168
+ existingSessionId: null,
169
+ appServerSessionId: null,
170
+ namespace,
171
+ drift: {
172
+ fields,
173
+ before: {
174
+ ...before,
175
+ provider_session_id: row.provider_session_id,
176
+ },
177
+ after,
178
+ },
179
+ };
180
+ }
181
+ return {
182
+ existingSessionId: row.provider_session_id,
183
+ appServerSessionId: row.app_server_session_id || null,
184
+ namespace,
185
+ drift: null,
186
+ providerSession: row,
187
+ };
188
+ }
189
+
98
190
  // ─── S2: session-config drift ────────────────────────────────────────
99
191
  //
100
192
  // A stored `sessions` row records the config the claude session was
@@ -136,6 +228,10 @@ function getClaudeSessionId(db, sessionKey) {
136
228
  // already covers cleanly. The stored model/effort columns are
137
229
  // informational, not identity.
138
230
  const SPAWN_IDENTITY_FIELDS = ['agent', 'cwd'];
231
+ // Codex model and effort are selected per turn and remain informational on
232
+ // the provider-session row. Workspace drift still changes the static runtime
233
+ // and security identity, so cwd is the sole invalidating field.
234
+ const CODEX_SPAWN_IDENTITY_FIELDS = ['cwd'];
139
235
 
140
236
  /**
141
237
  * Decide whether a stored session can be resumed for the next spawn,
@@ -215,7 +311,6 @@ function resolveSessionForSpawn(db, sessionKey, resolved = {}) {
215
311
  // before 0.12 and a row created today with pm_backend='cli' are
216
312
  // semantically the same in terms of session-context invariants
217
313
  // (bridge MCP server mounted, reply-tool contract enforced).
218
- const CHANNELS_CLASS = new Set(['channels', 'cli']);
219
314
  const wasChannelsClass = CHANNELS_CLASS.has(before.pm_backend);
220
315
  const willBeChannelsClass = CHANNELS_CLASS.has(after.pm_backend);
221
316
  if (after.pm_backend != null && wasChannelsClass !== willBeChannelsClass) {
@@ -244,4 +339,10 @@ module.exports = {
244
339
  resolveSessionForSpawn,
245
340
  countSessions,
246
341
  SPAWN_IDENTITY_FIELDS,
342
+ sessionNamespaceFor,
343
+ resolveProviderSessionForSpawn,
344
+ CLAUDE_INLINE_NAMESPACE,
345
+ CLAUDE_CHANNELS_NAMESPACE,
346
+ CODEX_APP_SERVER_NAMESPACE,
347
+ CODEX_SPAWN_IDENTITY_FIELDS,
247
348
  };