polygram 0.8.0 → 0.9.0-rc.2

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 (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/lib/{agent-loader.js → agents/loader.js} +6 -8
  3. package/lib/{approvals.js → approvals/store.js} +28 -5
  4. package/lib/{approval-ui.js → approvals/ui.js} +1 -17
  5. package/lib/config.js +121 -0
  6. package/lib/{error-classify.js → error/classify.js} +25 -34
  7. package/lib/handlers/abort.js +89 -0
  8. package/lib/handlers/approvals.js +361 -0
  9. package/lib/handlers/autosteer.js +94 -0
  10. package/lib/handlers/config-callback.js +118 -0
  11. package/lib/handlers/config-ui.js +104 -0
  12. package/lib/handlers/dispatcher.js +263 -0
  13. package/lib/handlers/download.js +182 -0
  14. package/lib/handlers/extract-attachments.js +97 -0
  15. package/lib/handlers/ipc-send.js +80 -0
  16. package/lib/handlers/poll.js +140 -0
  17. package/lib/handlers/record-inbound.js +88 -0
  18. package/lib/handlers/slash-commands.js +319 -0
  19. package/lib/handlers/voice.js +107 -0
  20. package/lib/pm-interface.js +27 -29
  21. package/lib/sdk/build-options.js +177 -0
  22. package/lib/sdk/callbacks.js +213 -0
  23. package/lib/{process-manager-sdk.js → sdk/process-manager.js} +19 -31
  24. package/lib/{telegram.js → telegram/api.js} +2 -2
  25. package/lib/{telegram-prompt.js → telegram/display-hint.js} +0 -14
  26. package/lib/{stream-reply.js → telegram/streamer.js} +4 -4
  27. package/package.json +2 -3
  28. package/polygram.js +347 -2581
  29. package/scripts/doctor.js +1 -1
  30. package/scripts/ipc-smoke.js +1 -10
  31. package/bin/approval-hook.js +0 -113
  32. package/lib/approval-waiters.js +0 -201
  33. package/lib/pm-router.js +0 -201
  34. package/lib/process-manager.js +0 -806
  35. /package/lib/{auto-resume.js → db/auto-resume.js} +0 -0
  36. /package/lib/{inbox.js → db/inbox.js} +0 -0
  37. /package/lib/{pairings.js → db/pairings.js} +0 -0
  38. /package/lib/{replay-window.js → db/replay-window.js} +0 -0
  39. /package/lib/{sent-cache.js → db/sent-cache.js} +0 -0
  40. /package/lib/{sessions.js → db/sessions.js} +0 -0
  41. /package/lib/{net-errors.js → error/net.js} +0 -0
  42. /package/lib/{ipc-client.js → ipc/client.js} +0 -0
  43. /package/lib/{ipc-file-validator.js → ipc/file-validator.js} +0 -0
  44. /package/lib/{ipc-server.js → ipc/server.js} +0 -0
  45. /package/lib/{telegram-chunk.js → telegram/chunk.js} +0 -0
  46. /package/lib/{deliver.js → telegram/deliver.js} +0 -0
  47. /package/lib/{telegram-format.js → telegram/format.js} +0 -0
  48. /package/lib/{parse-response.js → telegram/parse.js} +0 -0
  49. /package/lib/{status-reactions.js → telegram/reactions.js} +0 -0
  50. /package/lib/{typing-indicator.js → telegram/typing.js} +0 -0
  51. /package/lib/{voice.js → telegram/voice.js} +0 -0
package/scripts/doctor.js CHANGED
@@ -28,7 +28,7 @@ const Database = require('better-sqlite3');
28
28
 
29
29
  const {
30
30
  call, tell, socketPathFor, readSecret,
31
- } = require('../lib/ipc-client');
31
+ } = require('../lib/ipc/client');
32
32
 
33
33
  // ─── Arg parsing ─────────────────────────────────────────────────────
34
34
 
@@ -4,7 +4,7 @@
4
4
  * Usage: node scripts/ipc-smoke.js <bot-name>
5
5
  */
6
6
 
7
- const { call, socketPathFor } = require('../lib/ipc-client');
7
+ const { call, socketPathFor } = require('../lib/ipc/client');
8
8
 
9
9
  (async () => {
10
10
  const bot = process.argv[2] || 'shumabit';
@@ -12,15 +12,6 @@ const { call, socketPathFor } = require('../lib/ipc-client');
12
12
 
13
13
  console.log('path:', path);
14
14
  console.log('ping:', JSON.stringify(await call({ path, op: 'ping' })));
15
-
16
- console.log('ungated:', JSON.stringify(await call({
17
- path, op: 'approval_request',
18
- payload: {
19
- bot_name: bot, chat_id: '111111111',
20
- tool_name: 'Read', tool_input: { path: '/etc/hosts' },
21
- },
22
- })));
23
-
24
15
  console.log('DONE');
25
16
  })().catch((err) => {
26
17
  console.error('ERR:', err.message);
@@ -1,113 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Claude Code PreToolUse hook -> polygram daemon approval round-trip.
4
- *
5
- * Installed into an agent's settings.json:
6
- * { "hooks": { "PreToolUse": [
7
- * { "matcher": "Bash|WebFetch|mcp__*", "hooks": [
8
- * { "type": "command",
9
- * "command": "/Users/YOURNAME/polygram/bin/approval-hook.js" }
10
- * ]}
11
- * ]}}
12
- *
13
- * Environment (set by polygram when spawning Claude):
14
- * POLYGRAM_BOT - bot name owning this session (socket suffix)
15
- * POLYGRAM_CHAT_ID - chat whose message triggered this turn (for the card)
16
- * POLYGRAM_TURN_ID - optional; helps dedupe re-fires on Claude retries
17
- *
18
- * Contract (Claude Code):
19
- * stdin JSON: { session_id, hook_event_name: "PreToolUse",
20
- * tool_name, tool_input, ... }
21
- * stdout JSON reply for PreToolUse: either pass-through (exit 0 empty stdout),
22
- * or a block decision:
23
- * {"hookSpecificOutput": {"hookEventName":"PreToolUse",
24
- * "permissionDecision":"allow"|"deny"|"ask",
25
- * "permissionDecisionReason":"..."}}
26
- * Exit codes:
27
- * 0 - allow (empty stdout) or structured decision in stdout
28
- * 2 - block (deny)
29
- *
30
- * Failure policy: on IPC error (polygram down, socket missing, timeout) we
31
- * deny by default. Better to block a legitimate tool call than to let a
32
- * destructive one through when the approver is unreachable.
33
- */
34
-
35
- const fs = require('fs');
36
-
37
- (async () => {
38
- const botName = process.env.POLYGRAM_BOT;
39
- const chatId = process.env.POLYGRAM_CHAT_ID;
40
- const turnId = process.env.POLYGRAM_TURN_ID || null;
41
-
42
- if (!botName || !chatId) {
43
- deny('polygram-approval-hook: POLYGRAM_BOT and POLYGRAM_CHAT_ID env vars required');
44
- return;
45
- }
46
-
47
- let req;
48
- try {
49
- req = JSON.parse(fs.readFileSync(0, 'utf8'));
50
- } catch (err) {
51
- deny(`bad hook input: ${err.message}`);
52
- return;
53
- }
54
- if (req.hook_event_name !== 'PreToolUse') {
55
- // Not our event; pass through silently.
56
- process.exit(0);
57
- }
58
-
59
- // Resolve relative to this hook's own location rather than a hardcoded
60
- // absolute path — an absolute-path require is a symlink-swap RCE vector
61
- // (anyone who can write to that path gets code execution in-polygram).
62
- const path = require('path');
63
- const { call, socketPathFor, readSecret } = require(path.join(__dirname, '..', 'lib', 'ipc-client'));
64
- let res;
65
- try {
66
- res = await call({
67
- path: socketPathFor(botName),
68
- op: 'approval_request',
69
- secret: readSecret(botName),
70
- payload: {
71
- bot_name: botName,
72
- chat_id: chatId,
73
- turn_id: turnId,
74
- tool_name: req.tool_name,
75
- tool_input: req.tool_input,
76
- },
77
- });
78
- } catch (err) {
79
- deny(`polygram unreachable: ${err.message}`);
80
- return;
81
- }
82
-
83
- if (!res || !res.ok) {
84
- deny(`polygram error: ${res?.error || 'unknown'}`);
85
- return;
86
- }
87
-
88
- // Bridge signals one of: 'not-gated' | 'approved' | 'denied' | 'timeout' | 'auto-approved'
89
- if (res.decision === 'not-gated' || res.decision === 'approved' || res.decision === 'auto-approved') {
90
- // Pass through — let the default permission flow decide. An empty
91
- // stdout + exit 0 means "no opinion" from this hook.
92
- process.exit(0);
93
- }
94
-
95
- const reason = res.reason || `approval ${res.decision}`;
96
- deny(reason, res.decision);
97
- })().catch((err) => {
98
- deny(`hook crashed: ${err.message}`);
99
- });
100
-
101
- function deny(reason, decision = 'denied') {
102
- const out = {
103
- hookSpecificOutput: {
104
- hookEventName: 'PreToolUse',
105
- permissionDecision: 'deny',
106
- permissionDecisionReason: `[${decision}] ${reason}`,
107
- },
108
- };
109
- try {
110
- process.stdout.write(JSON.stringify(out));
111
- } catch {}
112
- process.exit(2);
113
- }
@@ -1,201 +0,0 @@
1
- /**
2
- * Parked-Promise Map for canUseTool's async user-approval flow.
3
- *
4
- * Per v4 plan §6.5.3 / Phase 1 step 8.
5
- *
6
- * Background: under SDK migration, canUseTool is an in-process
7
- * callback (replaces today's `bin/approval-hook.js` IPC). When a
8
- * gated tool fires, polygram posts a Telegram inline-keyboard card
9
- * to the admin chat and PARKS a Promise that resolves on user click.
10
- * The SDK awaits that Promise — so the in-flight tool sleeps until
11
- * the user decides.
12
- *
13
- * This module owns the waiter Map. Five cleanup paths are wired:
14
- * 1. resolveByClick(toolUseId, decision) — user pressed a button
15
- * 2. signal abort — SDK called Query.interrupt() / Query.close();
16
- * AbortSignal fires → Promise rejects with code:'ABORTED'
17
- * 3. timeout — periodic sweeper rejects waiters parked > timeoutMs
18
- * 4. rejectAllForSession(sessionKey) — pm.resetSession or kill
19
- * 5. shutdown — daemon SIGTERM; reject all
20
- *
21
- * Memory bound: MAX_WAITERS (200). Park beyond cap throws a typed
22
- * error so the caller can return `{behavior:'deny'}` to the SDK
23
- * instead of accumulating garbage.
24
- */
25
-
26
- 'use strict';
27
-
28
- const DEFAULT_MAX_WAITERS = 200;
29
- const DEFAULT_TIMEOUT_MS = 60_000; // 60s; matches OpenClaw cancel window
30
- const DEFAULT_SWEEP_INTERVAL_MS = 5_000;
31
-
32
- function createApprovalWaiters({
33
- logger = console,
34
- maxWaiters = DEFAULT_MAX_WAITERS,
35
- timeoutMs = DEFAULT_TIMEOUT_MS,
36
- sweepIntervalMs = DEFAULT_SWEEP_INTERVAL_MS,
37
- } = {}) {
38
- // toolUseId → entry { resolve, reject, signal, sigCleanup,
39
- // parkedAt, sessionKey }
40
- const waiters = new Map();
41
- let sweepTimer = null;
42
-
43
- /**
44
- * Park the canUseTool Promise; return a Promise that resolves on
45
- * user click / rejects on signal-abort / timeout / shutdown.
46
- *
47
- * @param {object} args
48
- * @param {string} args.toolUseId — SDK opts.toolUseID. Required.
49
- * @param {string} args.sessionKey — for rejectAllForSession routing.
50
- * @param {AbortSignal} [args.signal] — opts.signal from canUseTool.
51
- *
52
- * @returns {Promise<PermissionResult>}
53
- * @throws {Error{code:'WAITER_CAP'}} if cap exceeded.
54
- */
55
- function park({ toolUseId, sessionKey, signal }) {
56
- if (!toolUseId) {
57
- throw Object.assign(new Error('toolUseId required'),
58
- { code: 'NO_TOOL_USE_ID' });
59
- }
60
- if (waiters.size >= maxWaiters) {
61
- logger.error?.(`[approval-waiters] cap reached (${maxWaiters}); rejecting`);
62
- throw Object.assign(
63
- new Error(`approval waiter cap exceeded (${maxWaiters})`),
64
- { code: 'WAITER_CAP' },
65
- );
66
- }
67
- if (waiters.has(toolUseId)) {
68
- // Concurrent canUseTool with same toolUseID — SDK doesn't
69
- // typically retry the same call, but handle defensively by
70
- // resolving the old one with a deny first.
71
- logger.error?.(`[approval-waiters] duplicate toolUseId ${toolUseId}; abandoning prior waiter`);
72
- const prior = waiters.get(toolUseId);
73
- prior.reject(Object.assign(new Error('superseded'), { code: 'SUPERSEDED' }));
74
- }
75
-
76
- return new Promise((resolve, reject) => {
77
- // signal-abort cleanup wired here so signal-fires always
78
- // unparks the waiter, even if user click never arrives.
79
- const sigCleanup = signal
80
- ? () => {
81
- const e = waiters.get(toolUseId);
82
- if (e) {
83
- waiters.delete(toolUseId);
84
- e.reject(Object.assign(new Error('aborted'), { code: 'ABORTED' }));
85
- }
86
- }
87
- : null;
88
- if (signal && sigCleanup) {
89
- signal.addEventListener('abort', sigCleanup, { once: true });
90
- }
91
-
92
- waiters.set(toolUseId, {
93
- resolve: (decision) => {
94
- if (signal && sigCleanup) {
95
- try { signal.removeEventListener('abort', sigCleanup); }
96
- catch { /* swallow */ }
97
- }
98
- waiters.delete(toolUseId);
99
- resolve(decision);
100
- },
101
- reject: (err) => {
102
- if (signal && sigCleanup) {
103
- try { signal.removeEventListener('abort', sigCleanup); }
104
- catch { /* swallow */ }
105
- }
106
- waiters.delete(toolUseId);
107
- reject(err);
108
- },
109
- signal,
110
- parkedAt: Date.now(),
111
- sessionKey,
112
- });
113
-
114
- // If the signal was ALREADY aborted before we attached the
115
- // listener, addEventListener never fires — the waiter would
116
- // sit in the map until timeout-sweep / shutdown picked it up.
117
- // Trigger the cleanup manually so the parked promise rejects
118
- // immediately (matches "abort fired during park" semantics).
119
- if (signal && signal.aborted) sigCleanup();
120
- });
121
- }
122
-
123
- /**
124
- * Path 1: user clicked a button. `decision` is the
125
- * SDK-shape PermissionResult.
126
- */
127
- function resolveByClick(toolUseId, decision) {
128
- const e = waiters.get(toolUseId);
129
- if (!e) return false;
130
- e.resolve(decision);
131
- return true;
132
- }
133
-
134
- /**
135
- * Path 4: pm.resetSession or kill. Reject every waiter whose
136
- * sessionKey matches.
137
- */
138
- function rejectAllForSession(sessionKey, code = 'RESET_SESSION') {
139
- let count = 0;
140
- for (const [id, e] of [...waiters.entries()]) {
141
- if (e.sessionKey === sessionKey) {
142
- e.reject(Object.assign(new Error('session reset'), { code }));
143
- count++;
144
- }
145
- }
146
- return count;
147
- }
148
-
149
- /**
150
- * Path 5: daemon shutdown. Reject every waiter.
151
- */
152
- function rejectAll(code = 'DAEMON_SHUTDOWN') {
153
- let count = 0;
154
- for (const [id, e] of [...waiters.entries()]) {
155
- e.reject(Object.assign(new Error('daemon shutdown'), { code }));
156
- count++;
157
- }
158
- return count;
159
- }
160
-
161
- /**
162
- * Path 3: timeout sweeper. Periodically reject waiters parked
163
- * longer than timeoutMs.
164
- */
165
- function startTimeoutSweeper() {
166
- if (sweepTimer) return;
167
- const sweep = () => {
168
- const cutoff = Date.now() - timeoutMs;
169
- for (const [id, e] of [...waiters.entries()]) {
170
- if (e.parkedAt < cutoff) {
171
- e.reject(Object.assign(new Error('approval timeout'),
172
- { code: 'TIMEOUT' }));
173
- }
174
- }
175
- };
176
- sweepTimer = setInterval(sweep, sweepIntervalMs);
177
- sweepTimer.unref?.();
178
- }
179
-
180
- function stopTimeoutSweeper() {
181
- if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
182
- }
183
-
184
- return {
185
- park,
186
- resolveByClick,
187
- rejectAllForSession,
188
- rejectAll,
189
- startTimeoutSweeper,
190
- stopTimeoutSweeper,
191
- get size() { return waiters.size; },
192
- // Test introspection only:
193
- _waiters: waiters,
194
- };
195
- }
196
-
197
- module.exports = {
198
- createApprovalWaiters,
199
- DEFAULT_MAX_WAITERS,
200
- DEFAULT_TIMEOUT_MS,
201
- };
package/lib/pm-router.js DELETED
@@ -1,201 +0,0 @@
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
- /**
77
- * Broadcast helper for killChat / shutdown. Awaits every task to
78
- * settlement (success OR rejection), then throws an aggregate error
79
- * if any task rejected. Single rejections re-throw the original
80
- * error untouched (no AggregateError noise); multiple rejections
81
- * surface as `AggregateError` with all causes preserved.
82
- *
83
- * Each task entry is `[label, () => Promise]`; the label appears in
84
- * AggregateError messages so a debugger can tell which pm failed.
85
- */
86
- async function broadcastSettle(method, tasks) {
87
- const results = await Promise.allSettled(tasks.map(([, fn]) => fn()));
88
- const errors = [];
89
- results.forEach((r, i) => {
90
- if (r.status === 'rejected') {
91
- const tag = tasks[i][0];
92
- const err = r.reason instanceof Error ? r.reason : new Error(String(r.reason));
93
- err.pmTag = tag;
94
- errors.push(err);
95
- }
96
- });
97
- if (errors.length === 1) throw errors[0];
98
- if (errors.length > 1) {
99
- throw new AggregateError(errors, `${method} failed in ${errors.length} pms`);
100
- }
101
- }
102
-
103
- function createPmRouter({ cliPm, sdkPm = null, pickPmKindFor } = {}) {
104
- if (!cliPm) throw new TypeError('cliPm required');
105
- if (typeof pickPmKindFor !== 'function') {
106
- throw new TypeError('pickPmKindFor function required');
107
- }
108
-
109
- function routedPm(sessionKey) {
110
- return pickPmKindFor(sessionKey) === 'sdk' && sdkPm ? sdkPm : cliPm;
111
- }
112
-
113
- return {
114
- pickFor: routedPm,
115
- isSdkFor(sessionKey) {
116
- return pickPmKindFor(sessionKey) === 'sdk' && !!sdkPm;
117
- },
118
-
119
- // Methods that exist on every pm instance — direct routing.
120
- has(sessionKey) { return routedPm(sessionKey).has(sessionKey); },
121
- get(sessionKey) { return routedPm(sessionKey).get(sessionKey); },
122
- getOrSpawn(sessionKey, ctx) { return routedPm(sessionKey).getOrSpawn(sessionKey, ctx); },
123
- send(sessionKey, prompt, opts) { return routedPm(sessionKey).send(sessionKey, prompt, opts); },
124
- kill(sessionKey) { return routedPm(sessionKey).kill(sessionKey); },
125
-
126
- // Lifecycle methods broadcast to both pms because a chat may
127
- // have spawned sessions on either side at different times.
128
- // Promise.allSettled (NOT Promise.all) so a rejection from one
129
- // pm doesn't abandon the other mid-tear-down. Both must always
130
- // complete; we then surface aggregated errors. Pre-fix, a cliPm
131
- // rejection let sdkPm's Query.close() get GC'd with handles
132
- // still open.
133
- killChat(chatId) {
134
- const tasks = [['cli', () => cliPm.killChat(chatId)]];
135
- if (sdkPm) tasks.push(['sdk', () => sdkPm.killChat(chatId)]);
136
- return broadcastSettle('killChat', tasks);
137
- },
138
- shutdown() {
139
- const tasks = [['cli', () => cliPm.shutdown()]];
140
- if (sdkPm) tasks.push(['sdk', () => sdkPm.shutdown()]);
141
- return broadcastSettle('shutdown', tasks);
142
- },
143
-
144
- // Optional methods — forward when the routed pm implements
145
- // them, return a documented sentinel otherwise. Use
146
- // `pm.pickFor(sessionKey)` for proper feature detection at
147
- // call sites that need to branch on capability.
148
- steer(sessionKey, ...args) {
149
- const target = routedPm(sessionKey);
150
- return typeof target.steer === 'function' ? target.steer(sessionKey, ...args) : false;
151
- },
152
- // rc.42: native autosteer / queue. CLI pm doesn't have an
153
- // input-controller push primitive (the binary's stream-json
154
- // input is one-shot per pm.send), so it returns false. SDK pm
155
- // forwards to its inject implementation.
156
- injectUserMessage(sessionKey, opts) {
157
- const target = routedPm(sessionKey);
158
- return typeof target.injectUserMessage === 'function'
159
- ? target.injectUserMessage(sessionKey, opts)
160
- : false;
161
- },
162
- resetSession(sessionKey, opts) {
163
- const target = routedPm(sessionKey);
164
- return typeof target.resetSession === 'function'
165
- ? target.resetSession(sessionKey, opts)
166
- : Promise.resolve({ closed: false, drainedPendings: 0 });
167
- },
168
- applyFlagSettings(sessionKey, settings) {
169
- const target = routedPm(sessionKey);
170
- return typeof target.applyFlagSettings === 'function'
171
- ? target.applyFlagSettings(sessionKey, settings)
172
- : Promise.resolve(false);
173
- },
174
- setModel(sessionKey, model) {
175
- const target = routedPm(sessionKey);
176
- return typeof target.setModel === 'function'
177
- ? target.setModel(sessionKey, model)
178
- : Promise.resolve(false);
179
- },
180
- requestRespawn(sessionKey, reason) {
181
- const target = routedPm(sessionKey);
182
- return typeof target.requestRespawn === 'function'
183
- ? target.requestRespawn(sessionKey, reason)
184
- : { killed: false, queued: 0 };
185
- },
186
- drainQueue(sessionKey, errCode) {
187
- const target = routedPm(sessionKey);
188
- return typeof target.drainQueue === 'function'
189
- ? target.drainQueue(sessionKey, errCode)
190
- : 0;
191
- },
192
- interrupt(sessionKey) {
193
- const target = routedPm(sessionKey);
194
- return typeof target.interrupt === 'function'
195
- ? target.interrupt(sessionKey)
196
- : Promise.resolve();
197
- },
198
- };
199
- }
200
-
201
- module.exports = { makeRouterPolicy, createPmRouter };