@1presence/bridge 0.25.0 → 0.27.0

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.
@@ -9,6 +9,14 @@ function makeBridgeAccumulator() {
9
9
  toolResults: {},
10
10
  turns: [],
11
11
  };
12
+ // A new API turn bucket only opens when a user event (tool_results) has
13
+ // arrived since the last assistant event — mirrors the gateway translator
14
+ // logic. The CLI splits a single model emission's text and tool_use blocks
15
+ // into separate `{type:'assistant'}` events; without this guard each split
16
+ // creates a stored assistant message and produces consecutive same-role
17
+ // rows that the read-side merger has to repair. Init true so the very first
18
+ // assistant event opens turn 0.
19
+ let sawUserSinceLastAssistant = true;
12
20
  // Returns the current open API-turn bucket, creating one lazily if a text
13
21
  // event arrives before any `{type:'assistant'}` event (shouldn't normally
14
22
  // happen with Claude Code's stream-json, but keep it robust).
@@ -37,9 +45,14 @@ function makeBridgeAccumulator() {
37
45
  return;
38
46
  }
39
47
  if (type === 'assistant') {
40
- // New API turn — open a fresh bucket. All text and tool_use blocks
41
- // in this `assistant` event belong to this single API turn.
42
- state.turns.push({ text: '', toolUseIds: [] });
48
+ // Only open a fresh bucket when a user event has arrived since the
49
+ // last assistant event. Otherwise treat this as a continuation of
50
+ // the current API turn (CLI splits text and tool_use into separate
51
+ // assistant events within one model emission).
52
+ if (sawUserSinceLastAssistant) {
53
+ state.turns.push({ text: '', toolUseIds: [] });
54
+ sawUserSinceLastAssistant = false;
55
+ }
43
56
  const msg = event['message'];
44
57
  const content = msg?.['content'];
45
58
  if (!Array.isArray(content))
@@ -69,6 +82,8 @@ function makeBridgeAccumulator() {
69
82
  return;
70
83
  }
71
84
  if (type === 'user') {
85
+ // Flip the flag so the next assistant event opens a new API turn.
86
+ sawUserSinceLastAssistant = true;
72
87
  const msg = event['message'];
73
88
  const content = msg?.['content'];
74
89
  if (!Array.isArray(content))
package/dist/claude.js CHANGED
@@ -70,14 +70,17 @@ function spawnClaude(params) {
70
70
  existing.kill('SIGTERM');
71
71
  active.delete(conversationId);
72
72
  }
73
- const ctxParts = [];
74
- if (vaultFileOpen)
75
- ctxParts.push(`vault_file_open: ${vaultFileOpen}`);
76
- if (clientCapabilities?.length)
77
- ctxParts.push(`client_capabilities: ${clientCapabilities.join(', ')}`);
78
- if (syncedFolders?.length)
79
- ctxParts.push(`synced_folders: ${syncedFolders.join(', ')}`);
80
- const userMessageText = ctxParts.length > 0 ? `[${ctxParts.join(' | ')}]\n\n${text}` : text;
73
+ // Note: ephemeral context (vault_file_open / client_capabilities / synced_folders)
74
+ // is injected into the last user message by the gateway BEFORE history is
75
+ // sent over the WS. The bridge no longer constructs `userMessageText`
76
+ // `history` is the authoritative stream and already contains the new user
77
+ // prompt with prefix prepended. The `text`, `vaultFileOpen`,
78
+ // `clientCapabilities`, `syncedFolders` SpawnParams are retained for
79
+ // backward-compatible logging / spool correlation only.
80
+ void vaultFileOpen;
81
+ void clientCapabilities;
82
+ void syncedFolders;
83
+ void text;
81
84
  // Lockdown rationale:
82
85
  // - `--tools ""` disables ALL built-in tools (Bash/Read/Write/Edit/Glob/Grep/
83
86
  // WebFetch/etc.). MCP tools are not "built-in" so the 1Presence MCP surface
@@ -130,11 +133,14 @@ function spawnClaude(params) {
130
133
  stdio: ['pipe', 'pipe', 'pipe'],
131
134
  });
132
135
  active.set(conversationId, proc);
133
- // Feed prior turns + the new user message via stdin as stream-json.
134
- // Each line is a JSON object: `{ type, message: { role, content } }`.
135
- // Sanitisation (orphan tool_use stripping, displayOnly filtering) already
136
- // happened on the gateway via @presence/shared.toModelMessages replay
137
- // the history verbatim and append the fresh user turn.
136
+ // Feed the full conversation via stdin as stream-json. The gateway's
137
+ // early-save committed the new user message to Firestore BEFORE building
138
+ // `history`, so `history` already ends with the new user prompt (with the
139
+ // ephemeral context prefix prepended by the gateway). The bridge no longer
140
+ // appends a separate `newTurn` doing so would duplicate the user prompt.
141
+ // Sanitisation (orphan tool_use stripping, displayOnly filtering, consecutive
142
+ // same-role merging) already happened on the gateway via
143
+ // @presence/shared.toModelMessages — replay the history verbatim.
138
144
  try {
139
145
  const stdin = proc.stdin;
140
146
  if (!stdin) {
@@ -144,8 +150,6 @@ function spawnClaude(params) {
144
150
  const wrapped = { type: msg.role, message: { role: msg.role, content: msg.content } };
145
151
  stdin.write(JSON.stringify(wrapped) + '\n');
146
152
  }
147
- const newTurn = { type: 'user', message: { role: 'user', content: userMessageText } };
148
- stdin.write(JSON.stringify(newTurn) + '\n');
149
153
  stdin.end();
150
154
  }
151
155
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "bin": {
6
6
  "1presence-bridge": "dist/index.js"