polygram 0.9.0 → 0.10.0-rc.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/lib/db.js +14 -3
- package/lib/handlers/slash-commands.js +22 -12
- package/lib/model-costs.js +60 -0
- package/lib/process/factory.js +102 -0
- package/lib/process/process.js +193 -0
- package/lib/process/sdk-process.js +880 -0
- package/lib/process/tmux-process.js +1012 -0
- package/lib/process-manager.js +391 -0
- package/lib/sdk/callbacks.js +13 -5
- package/lib/tmux/log-tail.js +324 -0
- package/lib/tmux/orphan-sweep.js +79 -0
- package/lib/tmux/poll-scheduler.js +110 -0
- package/lib/tmux/session-log-parser.js +173 -0
- package/lib/tmux/tmux-runner.js +303 -0
- package/lib/tmux/tui-tool-input.js +62 -0
- package/migrations/011-pm-backend.sql +17 -0
- package/package.json +1 -1
- package/polygram.js +122 -33
- package/lib/sdk/process-manager.js +0 -1178
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0-rc.1",
|
|
5
5
|
"description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
package/lib/db.js
CHANGED
|
@@ -19,7 +19,7 @@ const Database = require('better-sqlite3');
|
|
|
19
19
|
// SCHEMA_VERSION; the early-return on line ~42 then skipped the
|
|
20
20
|
// migration loop on any DB already at user_version=8 → turn_metrics
|
|
21
21
|
// table never created → INSERT prepare at startup crashed polygram.
|
|
22
|
-
const SCHEMA_VERSION =
|
|
22
|
+
const SCHEMA_VERSION = 11;
|
|
23
23
|
|
|
24
24
|
// Sentinel `error` value for outbound rows whose API call may or may not
|
|
25
25
|
// have reached Telegram. markStalePending writes it; hasOutboundReplyTo
|
|
@@ -118,10 +118,10 @@ function wrap(db) {
|
|
|
118
118
|
const upsertSessionStmt = db.prepare(`
|
|
119
119
|
INSERT INTO sessions (
|
|
120
120
|
session_key, chat_id, thread_id, claude_session_id,
|
|
121
|
-
agent, cwd, model, effort, created_ts, last_active_ts
|
|
121
|
+
agent, cwd, model, effort, pm_backend, created_ts, last_active_ts
|
|
122
122
|
) VALUES (
|
|
123
123
|
@session_key, @chat_id, @thread_id, @claude_session_id,
|
|
124
|
-
@agent, @cwd, @model, @effort, @ts, @ts
|
|
124
|
+
@agent, @cwd, @model, @effort, @pm_backend, @ts, @ts
|
|
125
125
|
)
|
|
126
126
|
ON CONFLICT(session_key) DO UPDATE SET
|
|
127
127
|
chat_id = excluded.chat_id,
|
|
@@ -131,12 +131,14 @@ function wrap(db) {
|
|
|
131
131
|
cwd = excluded.cwd,
|
|
132
132
|
model = excluded.model,
|
|
133
133
|
effort = excluded.effort,
|
|
134
|
+
pm_backend = excluded.pm_backend,
|
|
134
135
|
last_active_ts = excluded.last_active_ts
|
|
135
136
|
`);
|
|
136
137
|
|
|
137
138
|
const getSessionStmt = db.prepare(`SELECT * FROM sessions WHERE session_key = ?`);
|
|
138
139
|
const touchSessionStmt = db.prepare(`UPDATE sessions SET last_active_ts = ? WHERE session_key = ?`);
|
|
139
140
|
const clearSessionIdStmt = db.prepare(`DELETE FROM sessions WHERE session_key = ?`);
|
|
141
|
+
const setSessionBackendStmt = db.prepare(`UPDATE sessions SET pm_backend = ? WHERE session_key = ?`);
|
|
140
142
|
|
|
141
143
|
const getMessageStmt = db.prepare(`
|
|
142
144
|
SELECT * FROM messages WHERE chat_id = ? AND msg_id = ?
|
|
@@ -291,6 +293,8 @@ function wrap(db) {
|
|
|
291
293
|
cwd: row.cwd || null,
|
|
292
294
|
model: row.model || null,
|
|
293
295
|
effort: row.effort || null,
|
|
296
|
+
// 0.10.0: pm_backend defaults to 'sdk' if caller doesn't set it.
|
|
297
|
+
pm_backend: row.pm_backend || 'sdk',
|
|
294
298
|
ts: row.ts || Date.now(),
|
|
295
299
|
});
|
|
296
300
|
},
|
|
@@ -307,6 +311,13 @@ function wrap(db) {
|
|
|
307
311
|
return clearSessionIdStmt.run(sessionKey);
|
|
308
312
|
},
|
|
309
313
|
|
|
314
|
+
// 0.10.0: backend reassignment without resetting other session fields.
|
|
315
|
+
// Used when ProcessManager spawns a Process with a different backend
|
|
316
|
+
// than the persisted row says (drift event fires too).
|
|
317
|
+
setSessionBackend(sessionKey, backend) {
|
|
318
|
+
return setSessionBackendStmt.run(backend, sessionKey);
|
|
319
|
+
},
|
|
320
|
+
|
|
310
321
|
getMessage(chatId, msgId) {
|
|
311
322
|
return getMessageStmt.get(String(chatId), msgId);
|
|
312
323
|
},
|
|
@@ -52,18 +52,24 @@ function createSlashCommands({
|
|
|
52
52
|
} = ctx;
|
|
53
53
|
const botAllowsCommands = !!config.bot?.allowConfigCommands;
|
|
54
54
|
|
|
55
|
-
// /context
|
|
55
|
+
// /context — route through pm.getContextUsage(sessionKey) so the
|
|
56
|
+
// call works for both SDK and tmux backends (the latter computes
|
|
57
|
+
// from JSONL message.usage). Pre-0.10.0-P0.2 this reached into
|
|
58
|
+
// entry.query.getContextUsage directly, which silently said "No
|
|
59
|
+
// active session yet" on tmux even when the chat was alive.
|
|
56
60
|
if (botAllowsCommands && text === '/context') {
|
|
57
|
-
|
|
58
|
-
const q = entry?.query;
|
|
59
|
-
if (!q || typeof q.getContextUsage !== 'function') {
|
|
61
|
+
if (!pm.has(sessionKey)) {
|
|
60
62
|
await sendReply('📚 No active session yet — send a message first, then /context.');
|
|
61
63
|
return true;
|
|
62
64
|
}
|
|
63
65
|
try {
|
|
64
|
-
const u = await
|
|
66
|
+
const u = await pm.getContextUsage(sessionKey);
|
|
65
67
|
await sendReply(formatContextReply(u));
|
|
66
68
|
} catch (err) {
|
|
69
|
+
if (err?.code === 'UNSUPPORTED_OPERATION' || err?.code === 'NOT_IMPLEMENTED_YET') {
|
|
70
|
+
await sendReply('📚 Context info not available yet — send a message first, then /context.');
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
67
73
|
logger.error?.(`[${label}] /context failed: ${err.message}`);
|
|
68
74
|
await sendReply(`📚 Couldn't fetch context info: ${err.message}`);
|
|
69
75
|
}
|
|
@@ -99,16 +105,20 @@ function createSlashCommands({
|
|
|
99
105
|
resumed_session_id: savedSessionId,
|
|
100
106
|
});
|
|
101
107
|
}
|
|
102
|
-
if (!entry
|
|
103
|
-
await sendReply('🗜️ Session not ready for /compact
|
|
108
|
+
if (!entry || typeof entry.fireUserMessage !== 'function') {
|
|
109
|
+
await sendReply('🗜️ Session not ready for /compact.');
|
|
104
110
|
return true;
|
|
105
111
|
}
|
|
106
112
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
// 0.10.0 P0.3 fix: route through Process.fireUserMessage so
|
|
114
|
+
// SDK (push to inputController) and tmux (paste to TUI) both
|
|
115
|
+
// handle the slash command. Pre-0.10.0-P0.3 reached into
|
|
116
|
+
// entry.inputController.push directly — broken on tmux.
|
|
117
|
+
const ok = entry.fireUserMessage(text);
|
|
118
|
+
if (!ok) {
|
|
119
|
+
await sendReply('🗜️ Session not ready for /compact.');
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
112
122
|
logEvent('compact-command', {
|
|
113
123
|
chat_id: chatId, thread_id: threadIdStr, session_key: sessionKey,
|
|
114
124
|
text_len: text.length,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic Claude API price rates per million tokens.
|
|
3
|
+
*
|
|
4
|
+
* Used by TmuxProcess to compute `cost_usd` per turn, since the
|
|
5
|
+
* per-session JSON log only carries token counts, not cost. SDK
|
|
6
|
+
* backend doesn't need this — Anthropic's claude-agent-sdk reports
|
|
7
|
+
* `total_cost_usd` directly in the result event.
|
|
8
|
+
*
|
|
9
|
+
* **Update reminder:** these rates can change. Last verified
|
|
10
|
+
* 2026-05-15 against https://www.anthropic.com/pricing. When rates
|
|
11
|
+
* shift, update the table here and bump the comment date.
|
|
12
|
+
*
|
|
13
|
+
* If a model isn't in the table, the `default` rates apply (Sonnet
|
|
14
|
+
* 4.6 today). Adding a new model is a 5-line PR.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// Per-million-token rates ($ USD). Cache-read and cache-creation
|
|
20
|
+
// follow Anthropic's prompt-caching pricing — read is ~10% of normal
|
|
21
|
+
// input; 1-hour creation is ~125% of normal input.
|
|
22
|
+
const MODEL_COSTS = {
|
|
23
|
+
// Claude Sonnet 4.6
|
|
24
|
+
'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreation: 3.75 },
|
|
25
|
+
// Claude Haiku 4.5 (date-suffixed model names map here via the
|
|
26
|
+
// `.replace(/-\d{8}$/, '')` strip in computeCostUsd; no need for
|
|
27
|
+
// a duplicate entry per snapshot).
|
|
28
|
+
'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreation: 1 },
|
|
29
|
+
// Claude Opus 4.7 (1M context)
|
|
30
|
+
'claude-opus-4-7': { input: 15, output: 75, cacheRead: 1.50, cacheCreation: 18.75 },
|
|
31
|
+
// Default fallback — Sonnet rates (safest mid-tier estimate).
|
|
32
|
+
default: { input: 3, output: 15, cacheRead: 0.30, cacheCreation: 3.75 },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute USD cost from a token-usage snapshot.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} usage — { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens }
|
|
39
|
+
* @param {string|null} model — e.g. 'claude-haiku-4-5-20251001'
|
|
40
|
+
* @returns {number} cost in USD; 0 if usage is missing
|
|
41
|
+
*/
|
|
42
|
+
function computeCostUsd(usage, model) {
|
|
43
|
+
if (!usage) return 0;
|
|
44
|
+
// Try exact match first; then prefix match (strip date suffix like -20251001).
|
|
45
|
+
let rate = MODEL_COSTS[model];
|
|
46
|
+
if (!rate && typeof model === 'string') {
|
|
47
|
+
const stripped = model.replace(/-\d{8}$/, '');
|
|
48
|
+
rate = MODEL_COSTS[stripped];
|
|
49
|
+
}
|
|
50
|
+
if (!rate) rate = MODEL_COSTS.default;
|
|
51
|
+
const M = 1_000_000;
|
|
52
|
+
return (
|
|
53
|
+
(usage.inputTokens || 0) * rate.input / M
|
|
54
|
+
+ (usage.outputTokens || 0) * rate.output / M
|
|
55
|
+
+ (usage.cacheReadTokens || 0) * rate.cacheRead / M
|
|
56
|
+
+ (usage.cacheCreationTokens || 0) * rate.cacheCreation / M
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { computeCostUsd, MODEL_COSTS };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process factory — chooses + constructs the right Process subclass
|
|
3
|
+
* per session, based on chat / topic / bot config.
|
|
4
|
+
*
|
|
5
|
+
* Backends:
|
|
6
|
+
* - 'sdk' → SdkProcess (default; long-lived SDK Query)
|
|
7
|
+
* - 'tmux' → TmuxProcess (claude TUI hosted in a tmux session)
|
|
8
|
+
*
|
|
9
|
+
* Backend selection precedence:
|
|
10
|
+
* topicConfig.pm > chatConfig.pm > config.bot.pm > 'sdk'
|
|
11
|
+
*
|
|
12
|
+
* The tmux backend requires a `tmuxRunner` + `botName` to be passed
|
|
13
|
+
* into createProcessFactory. If a tmux-routed chat is encountered
|
|
14
|
+
* without those wired, we log a loud warning and fall back to SDK so
|
|
15
|
+
* the daemon stays up (R2-F7 — never silent-fail config).
|
|
16
|
+
*
|
|
17
|
+
* @see docs/0.10.0-process-manager-abstraction-plan.md §6.4
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const { SdkProcess } = require('./sdk-process');
|
|
23
|
+
const { TmuxProcess } = require('./tmux-process');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
* @param {object} opts.config — runtime config object
|
|
28
|
+
* @param {Function} opts.spawnFn — buildSdkOptions (SDK backend only)
|
|
29
|
+
* @param {object} [opts.db] — for SdkProcess._logEvent + clearSessionId
|
|
30
|
+
* @param {object} [opts.logger]
|
|
31
|
+
* @param {number} [opts.queueCap]
|
|
32
|
+
* @param {number} [opts.queryCloseTimeoutMs]
|
|
33
|
+
* @param {object} [opts.tmuxRunner] — required when ANY chat routes to 'tmux'
|
|
34
|
+
* @param {string} [opts.botName] — required when ANY chat routes to 'tmux'
|
|
35
|
+
* @param {object} [opts.pollScheduler] — shared PollScheduler instance.
|
|
36
|
+
* When provided, all TmuxProcess instances share ONE setInterval for
|
|
37
|
+
* their polling loops (one timer regardless of how many in-flight
|
|
38
|
+
* tmux chats). Falls back to per-instance setTimeout when omitted.
|
|
39
|
+
* @returns {Function} processFactory(sessionKey, ctx) → Process
|
|
40
|
+
*/
|
|
41
|
+
function createProcessFactory({
|
|
42
|
+
config,
|
|
43
|
+
spawnFn,
|
|
44
|
+
db = null,
|
|
45
|
+
logger = console,
|
|
46
|
+
queueCap,
|
|
47
|
+
queryCloseTimeoutMs,
|
|
48
|
+
tmuxRunner = null,
|
|
49
|
+
botName = null,
|
|
50
|
+
pollScheduler = null,
|
|
51
|
+
} = {}) {
|
|
52
|
+
if (typeof spawnFn !== 'function') {
|
|
53
|
+
throw new TypeError('createProcessFactory: spawnFn required');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return function processFactory(sessionKey, ctx) {
|
|
57
|
+
const chatId = ctx?.chatId ?? null;
|
|
58
|
+
const threadId = ctx?.threadId ?? null;
|
|
59
|
+
const label = ctx?.label || sessionKey;
|
|
60
|
+
|
|
61
|
+
const choice = pickBackend({ config, chatId, threadId });
|
|
62
|
+
|
|
63
|
+
if (choice === 'tmux') {
|
|
64
|
+
if (!tmuxRunner || !botName) {
|
|
65
|
+
logger.warn?.(
|
|
66
|
+
`[${label}] config requests pm:'tmux' but tmuxRunner/botName not wired; ` +
|
|
67
|
+
`falling back to SdkProcess. Pass {tmuxRunner, botName} to createProcessFactory.`,
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
return new TmuxProcess({
|
|
71
|
+
sessionKey, chatId, threadId, label,
|
|
72
|
+
runner: tmuxRunner,
|
|
73
|
+
botName,
|
|
74
|
+
logger,
|
|
75
|
+
pollScheduler,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new SdkProcess({
|
|
81
|
+
sessionKey, chatId, threadId, label,
|
|
82
|
+
spawnFn,
|
|
83
|
+
db,
|
|
84
|
+
logger,
|
|
85
|
+
queueCap,
|
|
86
|
+
queryCloseTimeoutMs,
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Per-chat / per-topic backend choice. Phase 1 always returned 'sdk'.
|
|
93
|
+
* Phase 2 honors topicConfig.pm / chatConfig.pm / config.bot.pm.
|
|
94
|
+
*/
|
|
95
|
+
function pickBackend({ config, chatId, threadId }) {
|
|
96
|
+
if (!chatId) return 'sdk';
|
|
97
|
+
const chatCfg = config?.chats?.[chatId];
|
|
98
|
+
const topicCfg = threadId && chatCfg?.topics?.[threadId];
|
|
99
|
+
return topicCfg?.pm || chatCfg?.pm || config?.bot?.pm || 'sdk';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { createProcessFactory, pickBackend };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract Process — one running Claude session, regardless of backend.
|
|
3
|
+
*
|
|
4
|
+
* Subclasses ship per backend:
|
|
5
|
+
* - SdkProcess (lib/process/sdk-process.js) — long-lived
|
|
6
|
+
* @anthropic-ai/claude-agent-sdk Query
|
|
7
|
+
* - TmuxProcess (lib/process/tmux-process.js) — claude TUI hosted
|
|
8
|
+
* inside a tmux session (Phase 2)
|
|
9
|
+
*
|
|
10
|
+
* State machine: spawned → ready → (turn-in-flight | idle) ↔ closed.
|
|
11
|
+
*
|
|
12
|
+
* Public surface mirrors what polygram's handleMessage, slash-commands,
|
|
13
|
+
* autosteer, edit-correction etc. already call on the current SDK pm.
|
|
14
|
+
* Callers don't branch on subclass.
|
|
15
|
+
*
|
|
16
|
+
* Optional methods come in two flavors per the v3 audit:
|
|
17
|
+
* - Async ones MAY throw UnsupportedOperationError. Callers `await` +
|
|
18
|
+
* try/catch around them.
|
|
19
|
+
* - Sync HOT-PATH ones (drainQueue, injectUserMessage) return a
|
|
20
|
+
* sentinel value, NEVER throw. Per R1-F1: autosteer's call site
|
|
21
|
+
* has no try/catch — a throw would crash the message handler.
|
|
22
|
+
*
|
|
23
|
+
* Weighted LRU: each Process advertises its `cost`. The pm evicts
|
|
24
|
+
* to keep Σ cost ≤ budget rather than count ≤ cap. SDK Process cost=1,
|
|
25
|
+
* TmuxProcess cost=3 (per Phase 0 F-spike-2: tmux ~545MB vs SDK ~50MB
|
|
26
|
+
* per session).
|
|
27
|
+
*
|
|
28
|
+
* Phase 0 spike findings — `docs/0.10.0-phase0-spike-findings.md`.
|
|
29
|
+
* Spec — `docs/0.10.0-process-manager-abstraction-plan.md`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const EventEmitter = require('events');
|
|
35
|
+
|
|
36
|
+
class UnsupportedOperationError extends Error {
|
|
37
|
+
constructor(method, backend) {
|
|
38
|
+
super(`Operation ${method} not supported by ${backend} backend`);
|
|
39
|
+
this.name = 'UnsupportedOperationError';
|
|
40
|
+
this.code = 'UNSUPPORTED_OPERATION';
|
|
41
|
+
this.method = method;
|
|
42
|
+
this.backend = backend;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class Process extends EventEmitter {
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} opts
|
|
49
|
+
* @param {string} opts.sessionKey polygram session key
|
|
50
|
+
* @param {string|null} opts.chatId
|
|
51
|
+
* @param {string|null} opts.threadId
|
|
52
|
+
* @param {string} opts.label human-readable for logs
|
|
53
|
+
*/
|
|
54
|
+
constructor({ sessionKey, chatId, threadId, label } = {}) {
|
|
55
|
+
super();
|
|
56
|
+
if (typeof sessionKey !== 'string' || sessionKey.length === 0) {
|
|
57
|
+
throw new TypeError('Process: sessionKey (string) required');
|
|
58
|
+
}
|
|
59
|
+
// Identity — immutable after construction
|
|
60
|
+
this.sessionKey = sessionKey;
|
|
61
|
+
this.chatId = chatId == null ? null : String(chatId);
|
|
62
|
+
this.threadId = threadId == null ? null : String(threadId);
|
|
63
|
+
this.label = label || `${this.chatId || ''}${this.threadId ? '/' + this.threadId : ''}` || sessionKey;
|
|
64
|
+
// backend identifier — subclass overrides
|
|
65
|
+
this.backend = 'abstract';
|
|
66
|
+
|
|
67
|
+
// Mutable state
|
|
68
|
+
this.closed = false;
|
|
69
|
+
this.inFlight = false;
|
|
70
|
+
this.pendingQueue = [];
|
|
71
|
+
this.claudeSessionId = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Cost weight for LRU eviction (per Phase 0 F-spike-2).
|
|
76
|
+
* Subclass overrides. Defaults to 1 (SDK-equivalent).
|
|
77
|
+
*/
|
|
78
|
+
get cost() {
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── REQUIRED methods — subclass MUST override ─────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Cold-spawn this process. Wire up internals; mark ready when
|
|
86
|
+
* the underlying claude session is responsive.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} opts — backend-specific. Typically includes:
|
|
89
|
+
* existingSessionId — for --resume continuity
|
|
90
|
+
* model, effort, cwd, chatConfig, botName — spawn params
|
|
91
|
+
*/
|
|
92
|
+
async start(_opts) {
|
|
93
|
+
throw new Error(`${this.constructor.name}.start() must be overridden`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Send a user turn. Resolves with a PmSendResult on completion.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} prompt
|
|
100
|
+
* @param {object} [opts]
|
|
101
|
+
* @returns {Promise<PmSendResult>}
|
|
102
|
+
*/
|
|
103
|
+
async send(_prompt, _opts) {
|
|
104
|
+
throw new Error(`${this.constructor.name}.send() must be overridden`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Close cleanly. Returns when fully torn down.
|
|
109
|
+
* Idempotent.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} [reason]
|
|
112
|
+
*/
|
|
113
|
+
async kill(_reason) {
|
|
114
|
+
throw new Error(`${this.constructor.name}.kill() must be overridden`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── OPTIONAL async methods — caller awaits + try/catch ────────────
|
|
118
|
+
|
|
119
|
+
async interrupt() {
|
|
120
|
+
throw new UnsupportedOperationError('interrupt', this.backend);
|
|
121
|
+
}
|
|
122
|
+
async setModel(_model) {
|
|
123
|
+
throw new UnsupportedOperationError('setModel', this.backend);
|
|
124
|
+
}
|
|
125
|
+
async applyFlagSettings(_settings) {
|
|
126
|
+
throw new UnsupportedOperationError('applyFlagSettings', this.backend);
|
|
127
|
+
}
|
|
128
|
+
async setPermissionMode(_mode) {
|
|
129
|
+
throw new UnsupportedOperationError('setPermissionMode', this.backend);
|
|
130
|
+
}
|
|
131
|
+
async resetSession(_opts) {
|
|
132
|
+
throw new UnsupportedOperationError('resetSession', this.backend);
|
|
133
|
+
}
|
|
134
|
+
async getContextUsage() {
|
|
135
|
+
throw new UnsupportedOperationError('getContextUsage', this.backend);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── OPTIONAL sync HOT-PATH methods — never throw (R1-F1) ──────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reject all pending turns with the supplied error code.
|
|
142
|
+
* Used by /stop, daemon shutdown, /new.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} [_code='INTERRUPTED']
|
|
145
|
+
* @returns {number} count of pendings drained
|
|
146
|
+
*/
|
|
147
|
+
drainQueue(_code = 'INTERRUPTED') {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Inject a user message into the in-flight turn (autosteer +
|
|
153
|
+
* edit-correction). Returns false if the backend can't inject
|
|
154
|
+
* right now (e.g. no live turn) — caller falls through to normal
|
|
155
|
+
* pm.send queue path.
|
|
156
|
+
*
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
injectUserMessage(_opts) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Push priority='now' style steer (rare; legacy of OpenClaw shape).
|
|
165
|
+
* Hot-path-safe.
|
|
166
|
+
*
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
steer(_text, _opts) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fire-and-forget user-message injection regardless of inFlight
|
|
175
|
+
* state. Used by polygram's slash-command paths (/compact, /reload
|
|
176
|
+
* etc) where we want to send a user-shaped message into the
|
|
177
|
+
* underlying claude session BUT NOT wait for the turn to complete.
|
|
178
|
+
*
|
|
179
|
+
* Differs from `injectUserMessage` (which is for mid-stream fold and
|
|
180
|
+
* requires inFlight on tmux) and `send` (which blocks until turn
|
|
181
|
+
* completion). Default returns false; subclasses override.
|
|
182
|
+
*
|
|
183
|
+
* @returns {boolean} true if message was queued/pasted
|
|
184
|
+
*/
|
|
185
|
+
fireUserMessage(_text) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
Process,
|
|
192
|
+
UnsupportedOperationError,
|
|
193
|
+
};
|