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,50 @@
1
+ -- Provider/session compatibility namespaces.
2
+ --
3
+ -- The legacy sessions table remains unchanged for rollback. New binaries
4
+ -- dual-write Claude rows during the rollback window. Codex never writes the
5
+ -- legacy claude_session_id column.
6
+
7
+ CREATE TABLE agent_runtime_sessions (
8
+ session_key TEXT NOT NULL,
9
+ namespace TEXT NOT NULL
10
+ CHECK(namespace IN ('claude:inline', 'claude:channels', 'codex:app-server')),
11
+ provider TEXT NOT NULL CHECK(provider IN ('claude', 'codex')),
12
+ provider_session_id TEXT NOT NULL,
13
+ app_server_session_id TEXT,
14
+ agent TEXT,
15
+ cwd TEXT,
16
+ model TEXT,
17
+ effort TEXT,
18
+ pm_backend TEXT,
19
+ created_ts INTEGER NOT NULL,
20
+ last_active_ts INTEGER NOT NULL,
21
+ PRIMARY KEY(session_key, namespace)
22
+ );
23
+
24
+ CREATE INDEX idx_agent_runtime_sessions_provider
25
+ ON agent_runtime_sessions(provider, namespace, last_active_ts DESC);
26
+
27
+ -- Stored tmux rows are the historical inline backend. Only cli/channels rows
28
+ -- carry the Channels reply-tool compatibility contract.
29
+ INSERT INTO agent_runtime_sessions (
30
+ session_key, namespace, provider, provider_session_id,
31
+ app_server_session_id, agent, cwd, model, effort, pm_backend,
32
+ created_ts, last_active_ts
33
+ )
34
+ SELECT
35
+ session_key,
36
+ CASE
37
+ WHEN pm_backend IN ('cli', 'channels') THEN 'claude:channels'
38
+ ELSE 'claude:inline'
39
+ END,
40
+ 'claude',
41
+ claude_session_id,
42
+ NULL,
43
+ agent,
44
+ cwd,
45
+ model,
46
+ effort,
47
+ pm_backend,
48
+ created_ts,
49
+ last_active_ts
50
+ FROM sessions;
@@ -0,0 +1,211 @@
1
+ -- Durable Codex generation, request-delivery, effect, and recovery ledger.
2
+ -- Payload-bearing command/tool data is intentionally absent.
3
+
4
+ CREATE TABLE codex_runtime_identity (
5
+ singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
6
+ stable_host_id TEXT NOT NULL,
7
+ last_boot_session_id TEXT NOT NULL,
8
+ established_ts INTEGER NOT NULL,
9
+ updated_ts INTEGER NOT NULL
10
+ );
11
+
12
+ CREATE TABLE codex_reboot_releases (
13
+ stable_host_id TEXT NOT NULL,
14
+ incident_boot_session_id TEXT NOT NULL,
15
+ released_boot_session_id TEXT NOT NULL,
16
+ released_ts INTEGER NOT NULL,
17
+ PRIMARY KEY(stable_host_id, incident_boot_session_id)
18
+ );
19
+
20
+ CREATE TABLE codex_generations (
21
+ generation_id TEXT PRIMARY KEY,
22
+ session_key TEXT NOT NULL,
23
+ thread_id TEXT,
24
+ app_server_session_id TEXT,
25
+ stable_host_id TEXT NOT NULL,
26
+ boot_session_id TEXT NOT NULL,
27
+ state TEXT NOT NULL
28
+ CHECK(state IN (
29
+ 'created', 'active', 'healthy-stopped', 'containment-failed',
30
+ 'durability-blocked', 'retired'
31
+ )),
32
+ containment_reason TEXT,
33
+ created_ts INTEGER NOT NULL,
34
+ updated_ts INTEGER NOT NULL,
35
+ settled_ts INTEGER
36
+ );
37
+
38
+ CREATE INDEX idx_codex_generations_state
39
+ ON codex_generations(state, updated_ts);
40
+ CREATE INDEX idx_codex_generations_session
41
+ ON codex_generations(session_key, created_ts DESC);
42
+
43
+ CREATE TABLE codex_daemon_lease (
44
+ singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
45
+ generation_id TEXT REFERENCES codex_generations(generation_id)
46
+ ON DELETE SET NULL,
47
+ stable_host_id TEXT NOT NULL,
48
+ boot_session_id TEXT NOT NULL,
49
+ status TEXT NOT NULL CHECK(status IN ('clear', 'active', 'quarantined')),
50
+ quarantine_reason TEXT,
51
+ acquired_ts INTEGER,
52
+ updated_ts INTEGER NOT NULL,
53
+ released_ts INTEGER
54
+ );
55
+
56
+ CREATE TABLE codex_turn_attempts (
57
+ attempt_id TEXT PRIMARY KEY,
58
+ generation_id TEXT NOT NULL
59
+ REFERENCES codex_generations(generation_id) ON DELETE CASCADE,
60
+ session_key TEXT NOT NULL,
61
+ method TEXT NOT NULL,
62
+ thread_id TEXT,
63
+ turn_id TEXT,
64
+ telegram_source_message_id TEXT,
65
+ client_user_message_id TEXT,
66
+ request_id TEXT,
67
+ delivery_state TEXT NOT NULL
68
+ CHECK(delivery_state IN (
69
+ 'prepared', 'write-attempted', 'response-observed'
70
+ )),
71
+ response_outcome TEXT CHECK(response_outcome IN ('result', 'error')),
72
+ recovery_state TEXT NOT NULL
73
+ CHECK(recovery_state IN (
74
+ 'prepared', 'ambiguous', 'active', 'terminal-pending',
75
+ 'clean-pending', 'empty-registry-pending', 'settled', 'cancelled'
76
+ )),
77
+ terminal_status TEXT
78
+ CHECK(terminal_status IN ('completed', 'interrupted', 'failed')),
79
+ created_ts INTEGER NOT NULL,
80
+ updated_ts INTEGER NOT NULL,
81
+ settled_ts INTEGER,
82
+ ambiguous_ts INTEGER
83
+ );
84
+
85
+ CREATE INDEX idx_codex_attempts_recovery
86
+ ON codex_turn_attempts(recovery_state, updated_ts);
87
+ CREATE INDEX idx_codex_attempts_generation
88
+ ON codex_turn_attempts(generation_id, created_ts);
89
+
90
+ CREATE TABLE codex_attempt_checkpoints (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ generation_id TEXT NOT NULL
93
+ REFERENCES codex_generations(generation_id) ON DELETE CASCADE,
94
+ attempt_id TEXT
95
+ REFERENCES codex_turn_attempts(attempt_id) ON DELETE CASCADE,
96
+ kind TEXT NOT NULL,
97
+ thread_id TEXT,
98
+ turn_id TEXT,
99
+ request_id TEXT,
100
+ item_id TEXT,
101
+ detail_json TEXT,
102
+ ts INTEGER NOT NULL
103
+ );
104
+
105
+ CREATE INDEX idx_codex_checkpoints_attempt
106
+ ON codex_attempt_checkpoints(attempt_id, id);
107
+ CREATE INDEX idx_codex_checkpoints_generation
108
+ ON codex_attempt_checkpoints(generation_id, id);
109
+
110
+ -- Provider choice is recorded before provider dispatch so replay never infers
111
+ -- a backend from mutable current configuration. Historical rows without a
112
+ -- selection remain unknown.
113
+ CREATE TABLE inbound_runtime_selections (
114
+ bot_name TEXT NOT NULL,
115
+ telegram_chat_id TEXT NOT NULL,
116
+ telegram_message_id TEXT NOT NULL,
117
+ session_key TEXT NOT NULL,
118
+ provider TEXT NOT NULL CHECK(provider IN ('claude', 'codex')),
119
+ selected_ts INTEGER NOT NULL,
120
+ PRIMARY KEY(bot_name, telegram_chat_id, telegram_message_id)
121
+ );
122
+
123
+ CREATE INDEX idx_inbound_runtime_session
124
+ ON inbound_runtime_selections(session_key, provider, selected_ts);
125
+
126
+ CREATE TABLE codex_linked_inputs (
127
+ linked_input_id TEXT PRIMARY KEY,
128
+ generation_id TEXT NOT NULL
129
+ REFERENCES codex_generations(generation_id) ON DELETE CASCADE,
130
+ attempt_id TEXT NOT NULL
131
+ REFERENCES codex_turn_attempts(attempt_id) ON DELETE CASCADE,
132
+ target_attempt_id TEXT NOT NULL
133
+ REFERENCES codex_turn_attempts(attempt_id) ON DELETE CASCADE,
134
+ telegram_chat_id TEXT NOT NULL,
135
+ telegram_message_id TEXT NOT NULL,
136
+ state TEXT NOT NULL
137
+ CHECK(state IN ('linked', 'settled', 'failed', 'ambiguous', 'interrupted')),
138
+ created_ts INTEGER NOT NULL,
139
+ settled_ts INTEGER
140
+ );
141
+
142
+ CREATE INDEX idx_codex_linked_inputs_target
143
+ ON codex_linked_inputs(target_attempt_id, state);
144
+ CREATE INDEX idx_codex_linked_inputs_attempt
145
+ ON codex_linked_inputs(attempt_id, state);
146
+
147
+ -- One durable claim per Telegram input before either steering or queueing.
148
+ -- Payload text is deliberately absent; callers retain it only in memory.
149
+ CREATE TABLE codex_dispatch_reservations (
150
+ reservation_id TEXT PRIMARY KEY,
151
+ generation_id TEXT NOT NULL
152
+ REFERENCES codex_generations(generation_id) ON DELETE CASCADE,
153
+ session_key TEXT NOT NULL,
154
+ bot_name TEXT NOT NULL,
155
+ telegram_chat_id TEXT NOT NULL,
156
+ telegram_message_id TEXT NOT NULL,
157
+ state TEXT NOT NULL
158
+ CHECK(state IN (
159
+ 'reserved', 'steer-accepted', 'queue-authorized', 'ambiguous',
160
+ 'cancelled', 'settled', 'failed', 'interrupted'
161
+ )),
162
+ steer_attempt_id TEXT
163
+ REFERENCES codex_turn_attempts(attempt_id) ON DELETE CASCADE,
164
+ target_attempt_id TEXT
165
+ REFERENCES codex_turn_attempts(attempt_id) ON DELETE CASCADE,
166
+ created_ts INTEGER NOT NULL,
167
+ updated_ts INTEGER NOT NULL,
168
+ settled_ts INTEGER,
169
+ UNIQUE(bot_name, telegram_chat_id, telegram_message_id)
170
+ );
171
+
172
+ CREATE INDEX idx_codex_dispatch_generation
173
+ ON codex_dispatch_reservations(generation_id, state, created_ts);
174
+
175
+ CREATE TABLE codex_item_effects (
176
+ generation_id TEXT NOT NULL
177
+ REFERENCES codex_generations(generation_id) ON DELETE CASCADE,
178
+ attempt_id TEXT NOT NULL
179
+ REFERENCES codex_turn_attempts(attempt_id) ON DELETE CASCADE,
180
+ item_id TEXT NOT NULL,
181
+ item_type TEXT NOT NULL,
182
+ state TEXT NOT NULL CHECK(state IN ('started', 'completed', 'failed')),
183
+ created_ts INTEGER NOT NULL,
184
+ updated_ts INTEGER NOT NULL,
185
+ PRIMARY KEY(attempt_id, item_id)
186
+ );
187
+
188
+ CREATE INDEX idx_codex_item_effects_generation
189
+ ON codex_item_effects(generation_id, updated_ts);
190
+
191
+ CREATE TABLE codex_attempt_reconciliations (
192
+ attempt_id TEXT PRIMARY KEY
193
+ REFERENCES codex_turn_attempts(attempt_id) ON DELETE CASCADE,
194
+ disposition TEXT NOT NULL
195
+ CHECK(disposition IN ('incorporated', 'retry-authorized', 'dismissed')),
196
+ actor TEXT NOT NULL,
197
+ reason TEXT NOT NULL,
198
+ decided_ts INTEGER NOT NULL
199
+ );
200
+
201
+ CREATE TABLE codex_retry_reservations (
202
+ original_attempt_id TEXT PRIMARY KEY
203
+ REFERENCES codex_turn_attempts(attempt_id) ON DELETE CASCADE,
204
+ retry_attempt_id TEXT NOT NULL UNIQUE,
205
+ state TEXT NOT NULL
206
+ CHECK(state IN ('reserved', 'claimed', 'dispatched', 'retired')),
207
+ reserved_ts INTEGER NOT NULL,
208
+ claimed_ts INTEGER,
209
+ dispatched_ts INTEGER,
210
+ retired_ts INTEGER
211
+ );
@@ -0,0 +1,46 @@
1
+ -- Extend config_changes without changing its columns, so older Polygram
2
+ -- binaries can continue reading the table and writing legacy field values.
3
+ -- runMigrations() wraps every migration in BEGIN IMMEDIATE / COMMIT.
4
+
5
+ CREATE TABLE config_changes_v17 (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ chat_id TEXT NOT NULL,
8
+ thread_id TEXT,
9
+ field TEXT NOT NULL CHECK(field IN ('model','effort','agent','runtime','pm')),
10
+ old_value TEXT,
11
+ new_value TEXT NOT NULL,
12
+ user_id INTEGER,
13
+ user TEXT,
14
+ source TEXT,
15
+ ts INTEGER NOT NULL
16
+ );
17
+
18
+ INSERT INTO config_changes_v17 (
19
+ id,
20
+ chat_id,
21
+ thread_id,
22
+ field,
23
+ old_value,
24
+ new_value,
25
+ user_id,
26
+ user,
27
+ source,
28
+ ts
29
+ )
30
+ SELECT
31
+ id,
32
+ chat_id,
33
+ thread_id,
34
+ field,
35
+ old_value,
36
+ new_value,
37
+ user_id,
38
+ user,
39
+ source,
40
+ ts
41
+ FROM config_changes;
42
+
43
+ DROP TABLE config_changes;
44
+ ALTER TABLE config_changes_v17 RENAME TO config_changes;
45
+
46
+ CREATE INDEX idx_config_recent ON config_changes(chat_id, ts DESC);
package/ops/README.md CHANGED
@@ -102,9 +102,66 @@ await tell('admin-bot', 'sendMessage', {
102
102
  }, { source: 'cron:billing-sync' });
103
103
  ```
104
104
 
105
+ The daemon stores sockets and secrets in `<data-dir>/.ipc`, an
106
+ owner-only `0700` directory. External callers must either run with the same
107
+ working directory as the daemon or set `POLYGRAM_IPC_DIR` to that exact
108
+ canonical absolute directory. Temporary paths and unsafe aliases are rejected;
109
+ there is no `/tmp` fallback. If the data-directory path is too long for a
110
+ portable Unix socket, set `POLYGRAM_IPC_DIR` to a shorter owner-only path.
111
+
105
112
  Allowed methods: `sendMessage`, `sendPhoto`, `sendDocument`, `sendSticker`,
106
113
  `sendChatAction`, `editMessageText`, `setMessageReaction`. Other methods
107
114
  are rejected server-side.
108
115
 
109
116
  If the target bot is down, `tell()` throws. Intentional — cron failures
110
117
  should surface, not silently log to a DB the bot isn't watching.
118
+
119
+ ## Native Codex beta diagnostics
120
+
121
+ Codex is an opt-in native beta. Before selecting `pm: "codex"`, configure:
122
+
123
+ - `codex.binary` as the canonical immutable executable for the pinned version;
124
+ - `codex.home` as a dedicated, persistent, non-temporary `CODEX_HOME` with
125
+ mode `0700`;
126
+ - owner-only `config.toml` and `auth.json` files with mode `0600`; and
127
+ - the owned `polygram-session` profile with approval policy `never`, command
128
+ network disabled, and model web search disabled.
129
+
130
+ List every other daemon credential or control tree in
131
+ `codex.daemonSecretRoots`, including the interactive `~/.codex` tree when it
132
+ exists; the dedicated deployment home is separate and must not be reused.
133
+
134
+ Run the existing doctor against the intended config and database:
135
+
136
+ ```bash
137
+ node scripts/doctor.js --bot my-bot --config /path/to/config.json --db /path/to/my-bot.db
138
+ ```
139
+
140
+ The Codex checks are local and content-free. They verify the binary pin,
141
+ dedicated home, installed protocol schema, owned profile, protected IPC runtime
142
+ directory, stable host and kernel boot-session identity, and the daemon-wide
143
+ lease. They do not print
144
+ paths, credential contents, raw host/boot identities, generation IDs, prompts,
145
+ command output, or raw failure text. They do not start app-server or perform
146
+ authenticated model discovery. `codex-model-catalog` therefore remains a
147
+ warning until the normal pinned startup preflight proves the selected profile,
148
+ model, and effort are available.
149
+
150
+ Lease results have daemon scope:
151
+
152
+ - `clear` means this daemon has no live or quarantined native Codex owner.
153
+ - `active` means one chat owns the daemon-wide Codex generation; a second
154
+ Codex chat must be rejected without spawning or changing its session.
155
+ - `quarantined` is a hard failure. Do not delete the database, force-release
156
+ the lease, reset the chat, switch runtimes to evade it, or retry ambiguous
157
+ input automatically. Input reconciliation does not release containment.
158
+ The same validated host must reboot before native Codex can be released.
159
+
160
+ This lease does not fence Claude, another Polygram daemon, or an
161
+ uninstrumented host process.
162
+
163
+ Detached and background development servers are unsupported in the native
164
+ macOS beta. Healthy stop proves the exact Codex turn settled, tracked-terminal
165
+ cleanup was accepted, and a fresh registry page was empty; it does not prove
166
+ arbitrary descendants died. After transport or app-server hard loss, such a
167
+ process may survive until the required host reboot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
5
5
  "main": "lib/ipc/client.js",
6
6
  "bin": {
@@ -63,7 +63,7 @@
63
63
  "dependencies": {
64
64
  "@anthropic-ai/claude-agent-sdk": "0.2.123",
65
65
  "@modelcontextprotocol/sdk": "^1.29.0",
66
- "@shumkov/orchestra": "0.6.0",
66
+ "@shumkov/orchestra": "0.7.1",
67
67
  "better-sqlite3": "^12.9.0",
68
68
  "grammy": "^1.42.0",
69
69
  "markdown-it": "^14.1.1",