@chatpanel/bridge 0.11.6 → 0.11.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/bridge",
3
- "version": "0.11.6",
3
+ "version": "0.11.7",
4
4
  "type": "module",
5
5
  "description": "Local bridge that exposes the AI coding agents installed on your machine \u2014 Claude Code (CLI), Codex (CLI), and Antigravity CLI (formerly Gemini CLI, which remains available for business/enterprise) \u2014 to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
6
6
  "keywords": [
@@ -38,6 +38,29 @@ const HELP = [
38
38
  * it needs is injected — a bot token, the bridge address+token, a pairing store, an event sink
39
39
  * — so nothing here reads a secret or reaches for global state.
40
40
  */
41
+ // What a turn driven from a phone can actually DO, told to the agent up front.
42
+ //
43
+ // Without this the agent plans as if it were sitting at a terminal: asked to create a note it
44
+ // announced it would look for a notes folder, discovered writes were not enabled, said "let me
45
+ // check what notes tools are available" — and stopped mid-thought, having done nothing and
46
+ // explained nothing. The ceiling is real and enforced elsewhere (reach caps the toolset); the
47
+ // agent simply had no way to know about it, so it spent a turn discovering it and left the
48
+ // user staring at half a sentence.
49
+ export function channelSystem(userSystem, reach) {
50
+ const limits = !reach || reach === 'any' ? '' : [
51
+ "You are answering a message sent from the user's PHONE, relayed to this machine.",
52
+ reach === 'device'
53
+ ? 'You cannot read files, run commands, browse the web, or change anything.'
54
+ : "You can READ files and search the user's ChatPanel history. You cannot write or edit "
55
+ + 'files, run shell commands, or browse the web — those are switched off for messages '
56
+ + 'from a phone, and no tool will grant them.',
57
+ 'If a request needs something you cannot do, say so plainly in your FIRST reply and offer '
58
+ + 'what you can do instead. Never begin work you cannot finish.',
59
+ 'Answers are read on a phone: be brief, and put the answer first.',
60
+ ].join(' ');
61
+ return [limits, userSystem].filter(Boolean).join('\n\n');
62
+ }
63
+
41
64
  export function startTelegram({
42
65
  botToken,
43
66
  baseUrl,
@@ -54,6 +77,15 @@ export function startTelegram({
54
77
  model = '',
55
78
  provider = '',
56
79
  system = '',
80
+ // Read at the START OF EACH MESSAGE rather than captured when the loop starts.
81
+ //
82
+ // `chats` below holds every conversation's history AND its privacy vault, and it lives only
83
+ // as long as this loop. Settings used to be baked in at start, so changing the model — or
84
+ // the privacy mode, or anything else — had to stop and respawn the loop, which silently threw
85
+ // all of that away: the next message arrived as "a fresh session with no prior conversation",
86
+ // and PERSON_1 stopped meaning the same person. Switching which model answers is not a
87
+ // reason to forget what you were talking about.
88
+ route = null,
57
89
  redact = { tier: 'basic' },
58
90
  privacy = 'standard',
59
91
  logger = console,
@@ -120,7 +152,7 @@ export function startTelegram({
120
152
  }
121
153
  if (name === 'stop') {
122
154
  const st = chatState(norm.chatId);
123
- const ok = await transport.cancel(st.runId, { baseUrl, token });
155
+ const ok = await ((route && route().transport) || transport).cancel(st.runId, { baseUrl, token });
124
156
  st.runId = null;
125
157
  return void send(norm.chatId, ok ? '⏹ stopped.' : 'nothing running.');
126
158
  }
@@ -157,11 +189,22 @@ export function startTelegram({
157
189
  // on the bridge renders all-but-last as history and the last as "answer this now".
158
190
  const messages = [...st.history, { role: 'user', content: redacted }];
159
191
 
192
+ // Whatever the settings say RIGHT NOW — see `route` above.
193
+ const live = (route && route()) || {};
194
+ const useTransport = live.transport || transport;
160
195
  try {
161
- const finalState = await transport.chat(
162
- { agent, model, provider, system, messages, images, options: { reach } },
196
+ const finalState = await useTransport.chat(
197
+ {
198
+ agent: live.agent ?? agent,
199
+ model: live.model ?? model,
200
+ provider: live.provider ?? provider,
201
+ system: channelSystem(live.system ?? system, reach),
202
+ messages,
203
+ images,
204
+ options: { reach },
205
+ },
163
206
  {
164
- baseUrl, token, signal,
207
+ baseUrl: live.baseUrl || baseUrl, token, signal,
165
208
  onEvent: (ev, state) => {
166
209
  if (state.runId) st.runId = state.runId;
167
210
  // Driven by the folded STATE, not by an event's `type`: the bridge emits
@@ -132,14 +132,28 @@ export function createChannelService({
132
132
  function spawnLoop(botToken) {
133
133
  controller = new AbortController();
134
134
  running = true;
135
- // One choice, resolved here so the adapter never has to ask "which kind am I?".
136
- const viaGateway = !!settings.model;
135
+ // Resolved PER MESSAGE, not once at start. The loop owns every conversation's history and
136
+ // privacy vault, so restarting it to pick up a settings change threw away the thing the
137
+ // user came for. Now nothing needs a restart: the next message simply reads this.
138
+ const route = () => {
139
+ const viaGateway = !!settings.model;
140
+ return {
141
+ transport: viaGateway ? gateway : bridgeTransport,
142
+ agent: viaGateway ? '' : settings.agent,
143
+ model: viaGateway ? settings.model : '',
144
+ provider: viaGateway ? (settings.provider || '') : '',
145
+ baseUrl: viaGateway ? (settings.gatewayUrl || DEFAULT_GATEWAY_URL) : bridge.baseUrl,
146
+ system: settings.system || '',
147
+ privacy: settings.privacy,
148
+ };
149
+ };
137
150
  const done = startAdapter({
138
151
  botToken,
139
- transport: viaGateway ? gateway : bridgeTransport,
140
- model: viaGateway ? settings.model : '',
141
- provider: viaGateway ? (settings.provider || '') : '',
142
- baseUrl: viaGateway ? (settings.gatewayUrl || DEFAULT_GATEWAY_URL) : bridge.baseUrl,
152
+ route,
153
+ transport: route().transport,
154
+ model: route().model,
155
+ provider: route().provider,
156
+ baseUrl: route().baseUrl,
143
157
  token: bridge.token,
144
158
  pairing,
145
159
  savePairing,
@@ -258,8 +272,10 @@ export function createChannelService({
258
272
  if (patch.model) next.agent = '';
259
273
  settings = next;
260
274
  await saveSettings();
261
- // Settings are read when the loop starts, so a change only lands on a restart.
262
- if (running) { await this.stop(); await start(); }
275
+ // NO RESTART. The loop reads settings per message through `route`, and restarting it
276
+ // would discard every conversation's history and vault — which is exactly what made
277
+ // changing the model answer the next question with "this looks like a fresh session".
278
+ // The only change that still needs a restart is a new bot token, and connect() does that.
263
279
  return { settings: { ...settings } };
264
280
  },
265
281
 
package/src/server.js CHANGED
@@ -67,7 +67,7 @@ import {
67
67
  // Hardcoded (not read from package.json) so it survives Bun's single-file
68
68
  // --compile, where package.json isn't on a readable FS. CI fails the publish if
69
69
  // this drifts from package.json, so the two can't silently diverge.
70
- const VERSION = '0.11.6';
70
+ const VERSION = '0.11.7';
71
71
  const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
72
72
  const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
73
73