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.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/autosteer-buffer.js +52 -1
- package/lib/pm-interface.js +95 -0
- package/lib/pm-router.js +159 -0
- package/lib/process-manager-sdk.js +12 -0
- package/lib/process-manager.js +13 -0
- package/package.json +1 -1
- package/polygram.js +27 -127
|
@@ -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.
|
|
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",
|
package/lib/autosteer-buffer.js
CHANGED
|
@@ -77,4 +77,55 @@ function createAutosteerBuffer() {
|
|
|
77
77
|
return { append, drain, size, clear, formatForHook };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
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 = {};
|
package/lib/pm-router.js
ADDED
|
@@ -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,
|
package/lib/process-manager.js
CHANGED
|
@@ -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.
|
|
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
|
|
911
|
-
//
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
// context
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
const
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
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).
|
|
3719
|
-
//
|
|
3720
|
-
|
|
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)');
|