polygram 0.8.0-rc.16 → 0.8.0-rc.18

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "polygram",
4
- "version": "0.8.0-rc.16",
4
+ "version": "0.8.0-rc.18",
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 and a history skill.",
6
6
  "keywords": [
7
7
  "telegram",
@@ -77,4 +77,55 @@ function createAutosteerBuffer() {
77
77
  return { append, drain, size, clear, formatForHook };
78
78
  }
79
79
 
80
- module.exports = { createAutosteerBuffer };
80
+ /**
81
+ * Build the PostToolBatch hook callback that drains the buffer for
82
+ * a specific sessionKey on each tool boundary. The callback shape
83
+ * matches `@anthropic-ai/claude-agent-sdk`'s HookCallback contract
84
+ * (sdk.d.ts:726-728): returns a HookJSONOutput; never throws.
85
+ *
86
+ * @param {object} opts
87
+ * @param {object} opts.buffer — the per-session buffer instance
88
+ * @param {string} opts.sessionKey — closure-bound at Query spawn time
89
+ * @param {(kind: string, detail: object) => void} [opts.logEvent]
90
+ * — optional events.table emitter; called when a drain produces
91
+ * non-empty output, with kind='autosteer-hook-drained'.
92
+ * @param {string|null} [opts.chatId] — for the logEvent payload only.
93
+ * @param {object} [opts.logger] — for error logging (must have .error).
94
+ *
95
+ * @returns {async () => Promise<HookJSONOutput>}
96
+ */
97
+ function makePostToolBatchHook({ buffer, sessionKey, logEvent = null, chatId = null, logger = console } = {}) {
98
+ if (!buffer) throw new TypeError('buffer required');
99
+ if (!sessionKey) throw new TypeError('sessionKey required');
100
+ return async () => {
101
+ try {
102
+ const drained = buffer.drain(sessionKey);
103
+ if (drained.length === 0) return { continue: true };
104
+ const additionalContext = buffer.formatForHook(drained);
105
+ if (typeof logEvent === 'function') {
106
+ try {
107
+ logEvent('autosteer-hook-drained', {
108
+ chat_id: chatId,
109
+ session_key: sessionKey,
110
+ message_count: drained.length,
111
+ });
112
+ } catch { /* logger errors must not break the hook */ }
113
+ }
114
+ return {
115
+ continue: true,
116
+ hookSpecificOutput: {
117
+ hookEventName: 'PostToolBatch',
118
+ additionalContext,
119
+ },
120
+ };
121
+ } catch (err) {
122
+ logger?.error?.(`[${sessionKey}] PostToolBatch hook error: ${err?.message || err}`);
123
+ // Never throw out of a hook — the SDK may treat it as a hard
124
+ // fail (`stop_hook_prevented` result subtype). Drop the
125
+ // queued messages on the floor; the user can re-send.
126
+ return { continue: true };
127
+ }
128
+ };
129
+ }
130
+
131
+ module.exports = { createAutosteerBuffer, makePostToolBatchHook };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Canonical Pm interface (JSDoc typedef).
3
+ *
4
+ * Both `lib/process-manager.js` (CLI pm) and `lib/process-manager-sdk.js`
5
+ * (SDK pm) implement this. `lib/pm-router.js`'s `createPmRouter()`
6
+ * forwards calls to one or the other based on per-chat policy.
7
+ *
8
+ * Optional methods are marked `?` — the router exposes them too but
9
+ * returns documented sentinels when the routed pm doesn't implement
10
+ * them. Sites that need to feature-detect should call
11
+ * `pm.pickFor(sessionKey)` and probe `typeof X === 'function'` on
12
+ * the returned pm instance, not on the router.
13
+ *
14
+ * @typedef {object} PmEntry
15
+ * The shape pm.get(sessionKey) returns. Different pms decorate it
16
+ * with their own internal fields; only the documented fields below
17
+ * are part of the public contract.
18
+ * @property {string} sessionKey
19
+ * @property {string|null} chatId
20
+ * @property {string|null} threadId
21
+ * @property {boolean} closed
22
+ * @property {boolean} inFlight
23
+ * @property {Array<object>} pendingQueue — array of pending sends
24
+ *
25
+ * @typedef {object} PmSendResult
26
+ * The shape pm.send() resolves with on success. Failure rejects.
27
+ * @property {string} text — final assistant text (may be '')
28
+ * @property {string|null} sessionId — Claude session id (for resume)
29
+ * @property {number} cost — total cost USD
30
+ * @property {number} duration — turn duration ms
31
+ * @property {string|null} error — error string or null on success
32
+ * @property {object} metrics — token / tool / msg counts
33
+ * @property {number} metrics.inputTokens
34
+ * @property {number} metrics.outputTokens
35
+ * @property {number} metrics.cacheCreationTokens
36
+ * @property {number} metrics.cacheReadTokens
37
+ * @property {number} metrics.numAssistantMessages
38
+ * @property {number} metrics.numToolUses
39
+ * @property {string|null} metrics.resultSubtype
40
+ *
41
+ * @typedef {object} PmSendOptions
42
+ * @property {number} [timeoutMs]
43
+ * @property {number} [maxTurnMs]
44
+ * @property {object} [context] — opaque per-turn state (streamer, reactor, sourceMsgId)
45
+ *
46
+ * @typedef {object} PmSpawnContext
47
+ * What polygram passes to spawnFn(sessionKey, ctx). Internal to
48
+ * each pm but documented here so callers know what's available.
49
+ * @property {string|null} chatId
50
+ * @property {string|null} threadId
51
+ * @property {string} label
52
+ *
53
+ * @typedef {object} Pm
54
+ * The unified ProcessManager interface. Both CLI and SDK pm
55
+ * implement these; the router forwards.
56
+ *
57
+ * Required (every pm has these):
58
+ * @property {(sessionKey: string) => boolean} has
59
+ * @property {(sessionKey: string) => PmEntry|null} get
60
+ * @property {(sessionKey: string, ctx: PmSpawnContext) => Promise<PmEntry>} getOrSpawn
61
+ * @property {(sessionKey: string, prompt: string, opts?: PmSendOptions) => Promise<PmSendResult>} send
62
+ * @property {(sessionKey: string) => Promise<void>} kill
63
+ * @property {(chatId: string|number) => Promise<void>} killChat
64
+ * — closes ALL sessions belonging to a chat (broadcast across topics).
65
+ * @property {() => Promise<void>} shutdown
66
+ * — graceful daemon-wide drain + close.
67
+ *
68
+ * Optional (only one of the two pms implements these — feature-detect):
69
+ * @property {((sessionKey: string, text: string, opts?: object) => boolean)=} steer
70
+ * — SDK pm only (rc.9 PostToolBatch hook drains buffer).
71
+ * @property {((sessionKey: string, model: string) => Promise<boolean>)=} setModel
72
+ * — SDK pm only (Query.setModel live).
73
+ * @property {((sessionKey: string, settings: {effortLevel?: string}) => Promise<boolean>)=} applyFlagSettings
74
+ * — SDK pm only (Query.applyFlagSettings live).
75
+ * @property {((sessionKey: string, mode: string) => Promise<boolean>)=} setPermissionMode
76
+ * — SDK pm only.
77
+ * @property {((sessionKey: string, reason?: string) => {killed: boolean, queued: number})=} requestRespawn
78
+ * — CLI pm only (drain pending queue then kill; respawn on next send).
79
+ * @property {((sessionKey: string, errCode: string) => number)=} drainQueue
80
+ * — SDK pm only (reject all queued pendings with errCode).
81
+ * @property {((sessionKey: string) => Promise<void>)=} interrupt
82
+ * — SDK pm only (Query.interrupt — non-destructive).
83
+ * @property {((sessionKey: string, opts?: {reason?: string}) => Promise<{closed: boolean, drainedPendings: number}>)=} resetSession
84
+ * — SDK pm only (close Query + clear sessionId from DB).
85
+ *
86
+ * Lifecycle introspection (for tests / debugging — not required
87
+ * to be present, but both current pms expose them):
88
+ * @property {() => string[]=} keys — sessionKey list
89
+ * @property {() => number=} size — number of live sessions
90
+ */
91
+
92
+ // This file is JSDoc-only; no runtime exports. It exists so editors
93
+ // + the JSDoc-aware test mocks reference a single canonical type.
94
+
95
+ module.exports = {};
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Per-chat ProcessManager router (rc.6+).
3
+ *
4
+ * Daemon hosts up to TWO pm instances simultaneously — the
5
+ * stream-json-CLI ProcessManager and the @anthropic-ai/claude-agent-sdk
6
+ * ProcessManagerSdk. Each chat is assigned to one of them based on
7
+ * env config:
8
+ *
9
+ * POLYGRAM_USE_SDK=1 → all chats SDK pm
10
+ * POLYGRAM_SDK_CHATS=id1,id2,... → those chats SDK; others CLI
11
+ * neither set → all chats CLI
12
+ *
13
+ * The router exposes the same surface a single pm did, plus two
14
+ * introspection methods:
15
+ *
16
+ * pm.pickFor(sessionKey) → underlying pm instance (for feature
17
+ * detection at call sites)
18
+ * pm.isSdkFor(sessionKey) → boolean shortcut
19
+ *
20
+ * Lifecycle methods (`killChat`, `shutdown`) broadcast to BOTH pms
21
+ * when both are alive — a chat could have a session on either side
22
+ * (e.g. mid-config-change), so we don't risk leaking one.
23
+ *
24
+ * Optional methods (steer / setModel / applyFlagSettings /
25
+ * requestRespawn / drainQueue / interrupt / resetSession) forward
26
+ * when the routed pm has the method and return a sentinel otherwise.
27
+ * Sites that need to feature-detect should `pm.pickFor(sessionKey)`
28
+ * and check `typeof X === 'function'` directly.
29
+ *
30
+ * Used by `polygram.js` main() — Phase 5 + rc.6.
31
+ */
32
+
33
+ 'use strict';
34
+
35
+ /**
36
+ * Parse the SDK-chats env config into a router policy.
37
+ *
38
+ * @param {object} opts
39
+ * @param {boolean} opts.useSdkAll — POLYGRAM_USE_SDK=1
40
+ * @param {Iterable<string>} [opts.sdkChats] — POLYGRAM_SDK_CHATS list
41
+ * @param {(sessionKey: string) => string|null} opts.getChatIdFromKey
42
+ *
43
+ * @returns {object} { sdkAllChats, sdkSomeChats, sdkActive,
44
+ * sdkChatIdSet, pickPmKindFor }
45
+ */
46
+ function makeRouterPolicy({ useSdkAll = false, sdkChats = [], getChatIdFromKey } = {}) {
47
+ if (typeof getChatIdFromKey !== 'function') {
48
+ throw new TypeError('getChatIdFromKey function required');
49
+ }
50
+ const sdkChatIdSet = new Set(
51
+ [...sdkChats].map((s) => String(s).trim()).filter(Boolean),
52
+ );
53
+ const sdkAllChats = !!useSdkAll && sdkChatIdSet.size === 0;
54
+ const sdkSomeChats = sdkChatIdSet.size > 0;
55
+ const sdkActive = sdkAllChats || sdkSomeChats;
56
+
57
+ function pickPmKindFor(sessionKey) {
58
+ if (sdkAllChats) return 'sdk';
59
+ if (!sdkSomeChats) return 'cli';
60
+ const chatId = String(getChatIdFromKey(sessionKey) ?? '');
61
+ return sdkChatIdSet.has(chatId) ? 'sdk' : 'cli';
62
+ }
63
+
64
+ return { sdkAllChats, sdkSomeChats, sdkActive, sdkChatIdSet, pickPmKindFor };
65
+ }
66
+
67
+ /**
68
+ * Build a routing pm proxy. cliPm is required; sdkPm is optional
69
+ * (null when SDK isn't enabled for any chat).
70
+ *
71
+ * @param {object} opts
72
+ * @param {object} opts.cliPm
73
+ * @param {object|null} opts.sdkPm
74
+ * @param {(sessionKey: string) => 'sdk'|'cli'} opts.pickPmKindFor
75
+ */
76
+ function createPmRouter({ cliPm, sdkPm = null, pickPmKindFor } = {}) {
77
+ if (!cliPm) throw new TypeError('cliPm required');
78
+ if (typeof pickPmKindFor !== 'function') {
79
+ throw new TypeError('pickPmKindFor function required');
80
+ }
81
+
82
+ function routedPm(sessionKey) {
83
+ return pickPmKindFor(sessionKey) === 'sdk' && sdkPm ? sdkPm : cliPm;
84
+ }
85
+
86
+ return {
87
+ pickFor: routedPm,
88
+ isSdkFor(sessionKey) {
89
+ return pickPmKindFor(sessionKey) === 'sdk' && !!sdkPm;
90
+ },
91
+
92
+ // Methods that exist on every pm instance — direct routing.
93
+ has(sessionKey) { return routedPm(sessionKey).has(sessionKey); },
94
+ get(sessionKey) { return routedPm(sessionKey).get(sessionKey); },
95
+ getOrSpawn(sessionKey, ctx) { return routedPm(sessionKey).getOrSpawn(sessionKey, ctx); },
96
+ send(sessionKey, prompt, opts) { return routedPm(sessionKey).send(sessionKey, prompt, opts); },
97
+ kill(sessionKey) { return routedPm(sessionKey).kill(sessionKey); },
98
+
99
+ // Lifecycle methods broadcast to both pms because a chat may
100
+ // have spawned sessions on either side at different times.
101
+ async killChat(chatId) {
102
+ const tasks = [cliPm.killChat(chatId)];
103
+ if (sdkPm) tasks.push(sdkPm.killChat(chatId));
104
+ await Promise.all(tasks);
105
+ },
106
+ async shutdown() {
107
+ const tasks = [cliPm.shutdown()];
108
+ if (sdkPm) tasks.push(sdkPm.shutdown());
109
+ await Promise.all(tasks);
110
+ },
111
+
112
+ // Optional methods — forward when the routed pm implements
113
+ // them, return a documented sentinel otherwise. Use
114
+ // `pm.pickFor(sessionKey)` for proper feature detection at
115
+ // call sites that need to branch on capability.
116
+ steer(sessionKey, ...args) {
117
+ const target = routedPm(sessionKey);
118
+ return typeof target.steer === 'function' ? target.steer(sessionKey, ...args) : false;
119
+ },
120
+ resetSession(sessionKey, opts) {
121
+ const target = routedPm(sessionKey);
122
+ return typeof target.resetSession === 'function'
123
+ ? target.resetSession(sessionKey, opts)
124
+ : Promise.resolve({ closed: false, drainedPendings: 0 });
125
+ },
126
+ applyFlagSettings(sessionKey, settings) {
127
+ const target = routedPm(sessionKey);
128
+ return typeof target.applyFlagSettings === 'function'
129
+ ? target.applyFlagSettings(sessionKey, settings)
130
+ : Promise.resolve(false);
131
+ },
132
+ setModel(sessionKey, model) {
133
+ const target = routedPm(sessionKey);
134
+ return typeof target.setModel === 'function'
135
+ ? target.setModel(sessionKey, model)
136
+ : Promise.resolve(false);
137
+ },
138
+ requestRespawn(sessionKey, reason) {
139
+ const target = routedPm(sessionKey);
140
+ return typeof target.requestRespawn === 'function'
141
+ ? target.requestRespawn(sessionKey, reason)
142
+ : { killed: false, queued: 0 };
143
+ },
144
+ drainQueue(sessionKey, errCode) {
145
+ const target = routedPm(sessionKey);
146
+ return typeof target.drainQueue === 'function'
147
+ ? target.drainQueue(sessionKey, errCode)
148
+ : 0;
149
+ },
150
+ interrupt(sessionKey) {
151
+ const target = routedPm(sessionKey);
152
+ return typeof target.interrupt === 'function'
153
+ ? target.interrupt(sessionKey)
154
+ : Promise.resolve();
155
+ },
156
+ };
157
+ }
158
+
159
+ module.exports = { makeRouterPolicy, createPmRouter };
@@ -168,6 +168,18 @@ function makeInputController({ queueCap = DEFAULT_QUEUE_CAP } = {}) {
168
168
 
169
169
  // ─── ProcessManager ────────────────────────────────────────────────
170
170
 
171
+ /**
172
+ * @anthropic-ai/claude-agent-sdk-backed ProcessManager. Implements
173
+ * the canonical Pm interface (`lib/pm-interface.js`). Optional
174
+ * methods exposed: `steer`, `setModel`, `applyFlagSettings`,
175
+ * `setPermissionMode`, `drainQueue`, `interrupt`, `resetSession`.
176
+ *
177
+ * Optional methods NOT implemented (CLI pm has this): `requestRespawn`.
178
+ * For mid-session config changes use `applyFlagSettings` (effort)
179
+ * or `setModel`.
180
+ *
181
+ * @implements {import('./pm-interface.js').Pm}
182
+ */
171
183
  class ProcessManagerSdk {
172
184
  constructor({
173
185
  cap = DEFAULT_CAP,
@@ -93,6 +93,19 @@ function sumUsage(usageByMessage) {
93
93
  return out;
94
94
  }
95
95
 
96
+ /**
97
+ * Stream-json CLI-backed ProcessManager. Implements the canonical
98
+ * Pm interface (`lib/pm-interface.js`). Optional methods exposed:
99
+ * `requestRespawn` — drain queue and respawn process on next send
100
+ * (kept for parity with rc.6+ feature-detection at the router; SDK
101
+ * pm uses `applyFlagSettings` + `setModel` for the same UX).
102
+ *
103
+ * Optional methods NOT implemented (SDK pm has these): `steer`,
104
+ * `setModel`, `applyFlagSettings`, `setPermissionMode`,
105
+ * `drainQueue`, `interrupt`, `resetSession`.
106
+ *
107
+ * @implements {import('./pm-interface.js').Pm}
108
+ */
96
109
  class ProcessManager {
97
110
  constructor({
98
111
  cap = DEFAULT_CAP,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.8.0-rc.16",
3
+ "version": "0.8.0-rc.18",
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": {
package/polygram.js CHANGED
@@ -31,7 +31,8 @@ const { ProcessManager } = require('./lib/process-manager');
31
31
  // pick-at-startup. Phase 4 deletes the CLI version after Phase 5
32
32
  // soak proves SDK stable. See docs/0.8.0-architecture-decisions.md.
33
33
  const { ProcessManagerSdk } = require('./lib/process-manager-sdk');
34
- const { createAutosteerBuffer } = require('./lib/autosteer-buffer');
34
+ const { createAutosteerBuffer, makePostToolBatchHook } = require('./lib/autosteer-buffer');
35
+ const { makeRouterPolicy, createPmRouter } = require('./lib/pm-router');
35
36
  const agentLoader = require('./lib/agent-loader');
36
37
  const USE_SDK = process.env.POLYGRAM_USE_SDK === '1';
37
38
  const { createSender } = require('./lib/telegram');
@@ -907,39 +908,19 @@ function buildSdkOptions(sessionKey, ctx) {
907
908
  const useCanUseTool = apprCfg && apprCfg.adminChatId
908
909
  && Array.isArray(apprCfg.gatedTools) && apprCfg.gatedTools.length > 0;
909
910
 
910
- // 0.8.0-rc.9: PostToolBatch hook drains the autosteer buffer for
911
- // this session and injects queued user follow-ups as
912
- // `additionalContext` on each tool boundary. Framing matters:
913
- // wrapping in `<channel source="user-followup">…</channel>` is
914
- // what Claude is trained to trust as legitimate out-of-band user
915
- // context (verified live via post-tool-batch-spike-v2.mjs); the
916
- // earlier `<user_message_during_turn>` framing tripped the
917
- // model's prompt-injection defense and got refused.
918
- const postToolBatchHook = async () => {
919
- try {
920
- const drained = autosteerBuffer.drain(sessionKey);
921
- if (drained.length === 0) return { continue: true };
922
- const additionalContext = autosteerBuffer.formatForHook(drained);
923
- logEvent('autosteer-hook-drained', {
924
- chat_id: ctx?.chatId ?? null,
925
- session_key: sessionKey,
926
- message_count: drained.length,
927
- });
928
- return {
929
- continue: true,
930
- hookSpecificOutput: {
931
- hookEventName: 'PostToolBatch',
932
- additionalContext,
933
- },
934
- };
935
- } catch (err) {
936
- console.error(`[${sessionKey}] PostToolBatch hook error: ${err.message}`);
937
- // Never throw out of a hook — the SDK may treat it as a hard
938
- // fail (`stop_hook_prevented` result subtype). Drop the
939
- // queued messages on the floor; the user can re-send.
940
- return { continue: true };
941
- }
942
- };
911
+ // 0.8.0-rc.9 (factored to lib/autosteer-buffer.js in rc.17): the
912
+ // PostToolBatch hook drains the autosteer buffer for THIS session
913
+ // and injects queued user follow-ups as `additionalContext` on
914
+ // each tool boundary, wrapped in `<channel source="user-followup">`
915
+ // which Claude is trained to trust as legitimate out-of-band user
916
+ // context.
917
+ const postToolBatchHook = makePostToolBatchHook({
918
+ buffer: autosteerBuffer,
919
+ sessionKey,
920
+ chatId: ctx?.chatId ?? null,
921
+ logEvent,
922
+ logger: console,
923
+ });
943
924
 
944
925
  const baseOpts = {
945
926
  model: chatConfig.model || config.defaults.model,
@@ -3559,20 +3540,15 @@ async function main() {
3559
3540
  // battle-tested CLI path. When both pms run, killChat /shutdown
3560
3541
  // broadcast to both; everything else routes per-sessionKey via
3561
3542
  // pickPmFor() based on the chat's set membership.
3562
- const sdkChatIdSet = new Set(
3563
- String(process.env.POLYGRAM_SDK_CHATS || '')
3564
- .split(',').map((s) => s.trim()).filter(Boolean)
3565
- );
3566
- const sdkAllChats = USE_SDK && sdkChatIdSet.size === 0;
3567
- const sdkSomeChats = sdkChatIdSet.size > 0;
3568
- const sdkActive = sdkAllChats || sdkSomeChats;
3569
-
3570
- function pickPmKindFor(sessionKey) {
3571
- if (sdkAllChats) return 'sdk';
3572
- if (!sdkSomeChats) return 'cli';
3573
- const chatId = String(getChatIdFromKey(sessionKey) ?? '');
3574
- return sdkChatIdSet.has(chatId) ? 'sdk' : 'cli';
3575
- }
3543
+ // rc.17: router policy + proxy live in lib/pm-router.js for
3544
+ // testability. Policy parses env config and produces
3545
+ // pickPmKindFor; createPmRouter wraps the cli/sdk pms with the
3546
+ // routed surface.
3547
+ const { sdkActive, sdkAllChats, sdkSomeChats, sdkChatIdSet, pickPmKindFor } = makeRouterPolicy({
3548
+ useSdkAll: USE_SDK,
3549
+ sdkChats: String(process.env.POLYGRAM_SDK_CHATS || '').split(','),
3550
+ getChatIdFromKey,
3551
+ });
3576
3552
 
3577
3553
  // Shared callbacks: identical instance passed to both pms so a
3578
3554
  // chat's lifecycle events look the same regardless of which pm
@@ -3715,85 +3691,9 @@ async function main() {
3715
3691
  : null;
3716
3692
 
3717
3693
  // Routing pm: same surface as a single pm, but per-method routing
3718
- // through pickPmKindFor(sessionKey). Methods that don't take a
3719
- // sessionKey (killChat by chatId, shutdown) broadcast to both.
3720
- // For optional methods (steer / setModel / applyFlagSettings /
3721
- // requestRespawn / drainQueue / interrupt / resetSession) we
3722
- // forward when the routed pm has the method and return a
3723
- // sentinel otherwise — so feature-detection at the call site
3724
- // still works via `typeof pm.pickFor(sessionKey).X === 'function'`.
3725
- pm = (() => {
3726
- function routedPm(sessionKey) {
3727
- return pickPmKindFor(sessionKey) === 'sdk' && sdkPm ? sdkPm : cliPm;
3728
- }
3729
- const router = {
3730
- pickFor: routedPm,
3731
- isSdkFor(sessionKey) {
3732
- return pickPmKindFor(sessionKey) === 'sdk' && !!sdkPm;
3733
- },
3734
- has(sessionKey) { return routedPm(sessionKey).has(sessionKey); },
3735
- get(sessionKey) { return routedPm(sessionKey).get(sessionKey); },
3736
- getOrSpawn(sessionKey, ctx) { return routedPm(sessionKey).getOrSpawn(sessionKey, ctx); },
3737
- send(sessionKey, prompt, opts) { return routedPm(sessionKey).send(sessionKey, prompt, opts); },
3738
- kill(sessionKey) { return routedPm(sessionKey).kill(sessionKey); },
3739
- async killChat(chatId) {
3740
- const tasks = [cliPm.killChat(chatId)];
3741
- if (sdkPm) tasks.push(sdkPm.killChat(chatId));
3742
- await Promise.all(tasks);
3743
- },
3744
- async shutdown() {
3745
- const tasks = [cliPm.shutdown()];
3746
- if (sdkPm) tasks.push(sdkPm.shutdown());
3747
- await Promise.all(tasks);
3748
- },
3749
- // Optional methods. The router returns a function — but the
3750
- // function returns a sentinel if the routed pm doesn't have
3751
- // the method. Sites that want feature-detection should use
3752
- // `pm.pickFor(sessionKey)` and check `typeof X === 'function'`
3753
- // there instead of probing `pm.X` directly.
3754
- steer(sessionKey, ...args) {
3755
- const target = routedPm(sessionKey);
3756
- return typeof target.steer === 'function' ? target.steer(sessionKey, ...args) : false;
3757
- },
3758
- resetSession(sessionKey, opts) {
3759
- const target = routedPm(sessionKey);
3760
- return typeof target.resetSession === 'function'
3761
- ? target.resetSession(sessionKey, opts)
3762
- : Promise.resolve({ closed: false, drainedPendings: 0 });
3763
- },
3764
- applyFlagSettings(sessionKey, settings) {
3765
- const target = routedPm(sessionKey);
3766
- return typeof target.applyFlagSettings === 'function'
3767
- ? target.applyFlagSettings(sessionKey, settings)
3768
- : Promise.resolve(false);
3769
- },
3770
- setModel(sessionKey, model) {
3771
- const target = routedPm(sessionKey);
3772
- return typeof target.setModel === 'function'
3773
- ? target.setModel(sessionKey, model)
3774
- : Promise.resolve(false);
3775
- },
3776
- requestRespawn(sessionKey, reason) {
3777
- const target = routedPm(sessionKey);
3778
- return typeof target.requestRespawn === 'function'
3779
- ? target.requestRespawn(sessionKey, reason)
3780
- : { killed: false, queued: 0 };
3781
- },
3782
- drainQueue(sessionKey, errCode) {
3783
- const target = routedPm(sessionKey);
3784
- return typeof target.drainQueue === 'function'
3785
- ? target.drainQueue(sessionKey, errCode)
3786
- : 0;
3787
- },
3788
- interrupt(sessionKey) {
3789
- const target = routedPm(sessionKey);
3790
- return typeof target.interrupt === 'function'
3791
- ? target.interrupt(sessionKey)
3792
- : Promise.resolve();
3793
- },
3794
- };
3795
- return router;
3796
- })();
3694
+ // through pickPmKindFor(sessionKey). Per-method semantics
3695
+ // documented in lib/pm-router.js.
3696
+ pm = createPmRouter({ cliPm, sdkPm, pickPmKindFor });
3797
3697
 
3798
3698
  if (sdkAllChats) {
3799
3699
  console.log('[polygram] using SDK ProcessManager (all chats)');