backeer-opencode 0.8.1 → 0.10.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.
@@ -2,11 +2,12 @@ import { randomUUID } from "node:crypto"
2
2
  import { LiveControlClient, readLiveControlToken } from "./live-control-client.js"
3
3
  import { ConnectionError, loadConnectionProfiles, saveConnectionProfiles, connectionProfile } from "./live-profiles.js"
4
4
  import { discoverLiveConnections, discoveryDefaults } from "./live-discovery.js"
5
+ import { connectionDiagnostic } from "./live-diagnostics.js"
5
6
 
6
7
  // Bindings are immutable capabilities, kept in the calling chat, never a global "current session".
7
8
  // Each binding owns its SSE client/cursor, even when two chats target the same session.
8
9
  export class LiveSessionManager {
9
- constructor({ profilesFile, clientFactory = options => new LiveControlClient(options), maxBindings = 16,
10
+ constructor({ profilesFile, clientFactory = options => new LiveControlClient(options), maxBindings = Infinity,
10
11
  discovery = discoveryDefaults() }) {
11
12
  this.profilesFile = profilesFile
12
13
  this.clientFactory = clientFactory
@@ -18,6 +19,7 @@ export class LiveSessionManager {
18
19
  this.candidates = new Map()
19
20
  this.saving = Promise.resolve()
20
21
  this.shutdown = new AbortController()
22
+ this.pendingBindings = 0
21
23
  }
22
24
 
23
25
  async profiles() {
@@ -31,11 +33,12 @@ export class LiveSessionManager {
31
33
  return profile
32
34
  }
33
35
 
34
- async client(profile) {
35
- const token = await readLiveControlToken(profile.tokenFile)
36
+ async client(profile, sessionId) {
37
+ let token
38
+ try { token = await readLiveControlToken(profile.tokenFile) }
39
+ catch (error) { throw Object.assign(error, { connectionCode: connectionDiagnostic(error, { tokenFile: true }).code }) }
36
40
  if (this.closed) throw new ConnectionError("Connection manager is closed.")
37
- if (this.clients.size >= this.maxBindings) throw new ConnectionError("Connection limit reached. Disconnect an unused chat binding first.")
38
- const client = this.clientFactory({ url: profile.url, token, expectedDirectory: profile.directory })
41
+ const client = this.clientFactory({ url: profile.url, token, expectedDirectory: profile.directory, sessionId })
39
42
  this.clients.add(client)
40
43
  return client
41
44
  }
@@ -51,6 +54,7 @@ export class LiveSessionManager {
51
54
  try {
52
55
  const result = await client.listSessions()
53
56
  this.checkProject(profile, result)
57
+ this.checkSessionList(result)
54
58
  return { connection, ...result, sessions: result.sessions.filter(item => includeChildren || !item.parentID),
55
59
  subagentCount: result.sessions.filter(item => item.parentID).length }
56
60
  } finally { await this.release(client) }
@@ -58,26 +62,43 @@ export class LiveSessionManager {
58
62
 
59
63
  checkProject(profile, result) {
60
64
  if (result?.directory !== profile.directory) {
61
- throw new ConnectionError("Project verification failed. Check the tunnel target and exact project directory; the remote bridge must support project verification (0.6.0 or newer).")
65
+ throw Object.assign(new ConnectionError("Project verification failed. Check the tunnel target and exact project directory; the remote bridge must support project verification (0.6.0 or newer)."), { connectionCode: "project_mismatch" })
66
+ }
67
+ }
68
+
69
+ checkSessionList(result) {
70
+ if (!Array.isArray(result?.sessions) || result.sessions.some(item =>
71
+ typeof item?.id !== "string" || !/^[A-Za-z0-9_-]+$/.test(item.id) || typeof item.title !== "string")) {
72
+ throw Object.assign(new Error("Invalid bridge session list"), { connectionCode: "invalid_bridge_response" })
62
73
  }
63
74
  }
64
75
 
65
76
  async connect(connection, sessionId, { sessionTitle } = {}) {
77
+ if (this.closed) throw new ConnectionError("Connection manager is closed.")
78
+ if (this.bindings.size + this.pendingBindings >= this.maxBindings) {
79
+ throw new ConnectionError("Connection limit reached. Disconnect an unused chat binding first.")
80
+ }
81
+ this.pendingBindings++
82
+ try { return await this.connectSelected(connection, sessionId, { sessionTitle }) }
83
+ finally { this.pendingBindings-- }
84
+ }
85
+
86
+ async connectSelected(connection, sessionId, { sessionTitle } = {}) {
66
87
  if ((sessionId === undefined) === (sessionTitle === undefined)) {
67
88
  throw new ConnectionError("Choose exactly one session ID or exact session title from the current list.")
68
89
  }
69
90
  if (sessionTitle !== undefined) {
70
- if (typeof sessionTitle !== "string" || !sessionTitle.trim() || sessionTitle.length > 240) throw new ConnectionError("Invalid session title.")
91
+ if (typeof sessionTitle !== "string" || !sessionTitle.trim()) throw new ConnectionError("Invalid session title.")
71
92
  const result = await this.listSessions(connection)
72
93
  const matches = result.sessions.filter(item => item.title === sessionTitle)
73
94
  if (matches.length !== 1) throw new ConnectionError("Session title is missing or ambiguous. Refresh the session list and select an exact session ID.")
74
95
  sessionId = matches[0].id
75
96
  }
76
- if (typeof sessionId !== "string" || !/^[A-Za-z0-9_-]{1,160}$/.test(sessionId) || sessionId === "global") {
97
+ if (typeof sessionId !== "string" || !/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId === "global") {
77
98
  throw new ConnectionError("Invalid OpenCode session ID.")
78
99
  }
79
100
  const profile = await this.profile(connection)
80
- const client = await this.client(profile)
101
+ const client = await this.client(profile, sessionId)
81
102
  try {
82
103
  const session = await client.sessionStatus(sessionId)
83
104
  this.checkProject(profile, session)
@@ -85,7 +106,7 @@ export class LiveSessionManager {
85
106
  await client.start()
86
107
  if (this.closed) throw new ConnectionError("Connection manager is closed.")
87
108
  const bindingId = randomUUID()
88
- this.bindings.set(bindingId, { profile, sessionId, client })
109
+ this.bindings.set(bindingId, { profile, sessionId, client, session })
89
110
  return { bindingId, connection, session, cursor: client.cursor,
90
111
  connectionStatus: client.status, scope: "Keep this bindingId only in this chat. Other chats connect separately." }
91
112
  } catch (error) { await this.release(client); throw error }
@@ -113,7 +134,8 @@ export class LiveSessionManager {
113
134
 
114
135
  async status(bindingId) {
115
136
  const value = await this.verified(bindingId)
116
- return { bindingId, connection: value.profile.name, session: value.session, connectionStatus: value.client.status }
137
+ return { bindingId, connection: value.profile.name, session: value.session, verified: true,
138
+ connectionStatus: value.client.status, transport: value.client.connectionState() }
117
139
  }
118
140
 
119
141
  async send(bindingId, { requestId, prompt }, signal) {
@@ -132,8 +154,68 @@ export class LiveSessionManager {
132
154
  }
133
155
 
134
156
  async read(bindingId, options, waitMs = 0, signal) {
135
- const value = await this.verified(bindingId)
136
- return value.client.readEvents({ ...options, sessionId: value.sessionId }, { waitMs, signal })
157
+ let value
158
+ try { value = await this.verified(bindingId) }
159
+ catch (error) {
160
+ // A temporary tunnel outage must not hide output already buffered for this
161
+ // immutable binding. Permission/project/session failures still fail closed.
162
+ if (!error.connectionCode || !connectionDiagnostic(error).retryable) throw error
163
+ value = this.binding(bindingId)
164
+ const current = await this.profile(value.profile.name)
165
+ if (JSON.stringify(current) !== JSON.stringify(value.profile)) throw new ConnectionError("Connection profile changed. Disconnect and reconnect this chat to verify the new target.")
166
+ const result = value.client.snapshot({ ...options, sessionId: value.sessionId })
167
+ return { ...result, verified: false, diagnostic: connectionDiagnostic(error),
168
+ nextStep: "Keep this binding and cursor. The stream retries every 3 seconds; verify the same session after reconnection before sending or acknowledging a gap." }
169
+ }
170
+ const result = await value.client.readEvents({ ...options, sessionId: value.sessionId }, { waitMs, signal })
171
+ return { ...result, verified: true }
172
+ }
173
+
174
+ async diagnose({ connection, bindingId } = {}) {
175
+ if ((connection === undefined) === (bindingId === undefined)) throw new ConnectionError("Choose one saved connection or one chat binding to diagnose.")
176
+ const bound = bindingId ? this.binding(bindingId) : undefined
177
+ const profile = bound?.profile ?? await this.profile(connection)
178
+ let client
179
+ try {
180
+ if (bound) {
181
+ const result = await this.status(bindingId)
182
+ return { ...result, healthy: result.connectionStatus === "connected" }
183
+ }
184
+ client = await this.client(profile)
185
+ const result = await client.listSessions()
186
+ this.checkProject(profile, result)
187
+ this.checkSessionList(result)
188
+ return { connection: profile.name, directory: profile.directory, healthy: true, verified: true,
189
+ nextStep: "Choose an existing session by title. No remote work was changed." }
190
+ } catch (error) {
191
+ // Local profile errors should retain their own precise safe message.
192
+ if (error instanceof ConnectionError && !error.connectionCode) throw error
193
+ return { connection: profile.name, ...(bindingId ? { bindingId } : {}), healthy: false, verified: false,
194
+ diagnostic: connectionDiagnostic(error), ...(bound ? { transport: bound.client.connectionState() } : {}) }
195
+ } finally { if (client) await this.release(client) }
196
+ }
197
+
198
+ async setup({ connection, url, tokenFile } = {}, signal) {
199
+ if (connection !== undefined) {
200
+ if (url !== undefined || tokenFile !== undefined) throw new ConnectionError("Choose a saved connection or discovery inputs, not both.")
201
+ try {
202
+ const result = await this.listSessions(connection)
203
+ return { stage: "choose_session", ...result, nextTool: "connect_session", requiresSelection: true,
204
+ nextStep: result.sessions.length ? "Ask the user to choose a session by title, then connect this chat. Never select the newest automatically."
205
+ : "No main sessions are available. Keep existing work intact; ask the user which session to use." }
206
+ } catch (error) {
207
+ if (error instanceof ConnectionError && !error.connectionCode) throw error
208
+ return { stage: "fix_connection", connection, diagnostic: connectionDiagnostic(error), nextTool: "diagnose_connection" }
209
+ }
210
+ }
211
+ const profiles = await this.profiles()
212
+ if (profiles.length && url === undefined && tokenFile === undefined) {
213
+ return { stage: "choose_connection", connections: profiles.map(({ name, url, directory }) => ({ name, url, directory })),
214
+ nextTool: "connection_setup", requiresSelection: true, nextStep: "Ask the user to select the intended connection, then call connection_setup with its name." }
215
+ }
216
+ const result = await this.discover({ url, tokenFile }, signal)
217
+ return { stage: result.candidates.length ? "choose_candidate" : "fix_connection", ...result,
218
+ nextTool: result.candidates.length ? "save_connection" : "discover_connections", requiresSelection: true }
137
219
  }
138
220
 
139
221
  async disconnect(bindingId) {
@@ -160,7 +242,6 @@ export class LiveSessionManager {
160
242
  for (const candidate of result.candidates) {
161
243
  let id = [...this.candidates].find(([, item]) => JSON.stringify(item.profile) === JSON.stringify(candidate.profile))?.[0]
162
244
  if (!id) {
163
- if (this.candidates.size >= 32) throw new ConnectionError("Discovery candidate limit reached. Restart discovery in a new MCP process.")
164
245
  id = randomUUID()
165
246
  }
166
247
  this.candidates.set(id, candidate)
@@ -168,6 +249,7 @@ export class LiveSessionManager {
168
249
  url: candidate.profile.url, directory: candidate.profile.directory, sessions: candidate.sessions, subagentCount: candidate.subagentCount })
169
250
  }
170
251
  return { candidates, diagnostics: candidates.length ? [] : result.diagnostics,
252
+ diagnosticDetails: candidates.length ? [] : result.diagnosticDetails,
171
253
  nextStep: candidates.length ? "Select the intended connection, save it with a short name, then choose a session by title."
172
254
  : "Open the intended SSH tunnel and select an existing protected live-control token file. Do not paste credentials into chat." }
173
255
  }
@@ -179,7 +261,11 @@ export class LiveSessionManager {
179
261
  if (!candidate || this.closed) throw new ConnectionError("Discovery candidate not found. Discover connections again.")
180
262
  const profile = connectionProfile({ ...candidate.profile, name })
181
263
  const client = await this.client(profile)
182
- try { this.checkProject(profile, await client.listSessions()) }
264
+ try {
265
+ const result = await client.listSessions()
266
+ this.checkProject(profile, result)
267
+ this.checkSessionList(result)
268
+ }
183
269
  finally { await this.release(client) }
184
270
  if (this.closed) throw new ConnectionError("Connection manager is closed.")
185
271
  const profiles = await this.profiles()
@@ -14,9 +14,9 @@ async function commonDirectory(directory) {
14
14
  // Follow only an already observed exact session into a real worktree of the
15
15
  // same Git repository. A path prefix or a remote projectID alone is not proof.
16
16
  export function approvedWorktreeSessions(value) {
17
- if (!Array.isArray(value) || value.length > 16) return []
18
- return value.filter(item => /^[A-Za-z0-9_-]{1,160}$/.test(item?.sessionID) && item.sessionID !== "global" &&
19
- typeof item.directory === "string" && isAbsolute(item.directory) && item.directory.length <= 4096)
17
+ if (!Array.isArray(value)) return []
18
+ return value.filter(item => /^[A-Za-z0-9_-]+$/.test(item?.sessionID) && item.sessionID !== "global" &&
19
+ typeof item.directory === "string" && isAbsolute(item.directory))
20
20
  }
21
21
 
22
22
  export function createSessionScope(directory, { resolveCommon = commonDirectory, approvedSessions = [] } = {}) {
@@ -28,7 +28,6 @@ export function createSessionScope(directory, { resolveCommon = commonDirectory,
28
28
  if (location === directory) {
29
29
  if (info.id) {
30
30
  observed.set(info.id, info.projectID)
31
- if (observed.size > 256) observed.delete(observed.keys().next().value)
32
31
  }
33
32
  return true
34
33
  }
@@ -28,14 +28,20 @@ export async function ownLoopbackUrls({ platform = process.platform, fs = { read
28
28
  }
29
29
 
30
30
  export async function privateEndpoint({ argv = process.argv, env = process.env, pid = process.pid,
31
+ platform = process.platform, initialEnvironment = () => readFile("/proc/self/environ", "utf8"),
31
32
  urls = ownLoopbackUrls, request = fetch } = {}) {
32
33
  if (!argv.includes("--stdio")) return undefined
33
34
  const flag = argv.indexOf("--port")
34
35
  const rawPort = flag >= 0 ? argv[flag + 1] : argv.find(arg => arg.startsWith("--port="))?.slice(7)
35
36
  const port = typeof rawPort === "string" && /^\d+$/.test(rawPort) ? Number(rawPort) : NaN
36
- const password = env.OPENCODE_PASSWORD
37
37
  try {
38
- if (!Number.isInteger(port) || port < 0 || port > 65535 || !password) throw new Error()
38
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error()
39
+ // OpenCode removes this variable at runtime. Linux retains this process's
40
+ // initial environment; read only our own existing password, never another PID.
41
+ const password = env.OPENCODE_PASSWORD || (platform === "linux"
42
+ ? (await initialEnvironment()).split("\0").find(value => value.startsWith("OPENCODE_PASSWORD="))?.slice(18)
43
+ : undefined)
44
+ if (!password) throw new Error()
39
45
  const candidates = port === 0 ? await urls() : [`http://127.0.0.1:${port}`]
40
46
  for (const url of candidates) {
41
47
  const endpoint = { url, auth: { type: "basic", username: "opencode", password } }
@@ -10,7 +10,7 @@ import {
10
10
  validateQuestionCommand,
11
11
  } from "./question-command.js"
12
12
  import { runCommandStream } from "./sse.js"
13
- import { rememberBounded } from "./utilities.js"
13
+ import { remember } from "./utilities.js"
14
14
 
15
15
  async function replyToOpenCode(client, pending, decision) {
16
16
  const result = await client.postSessionIdPermissionsPermissionId({
@@ -110,7 +110,7 @@ export async function replyToQuestion(client, pending, response) {
110
110
 
111
111
  async function postQuestionState(config, eventID, path, body = {}) {
112
112
  const url = new URL(`/v1/events/${encodeURIComponent(eventID)}/${path}`, config.origin)
113
- await postJsonWithRetry(config, url, body, { retryDelays: [0, 500] })
113
+ await postJsonWithRetry(config, url, body)
114
114
  }
115
115
 
116
116
  export function createPermissionController({
@@ -173,7 +173,7 @@ export function createPermissionController({
173
173
  async function handleCommand(command) {
174
174
  if (!command || typeof command !== "object") return
175
175
  if (!["opencode_permission_reply", "opencode_question_reply"].includes(command.type)) return
176
- if (typeof command.id !== "string" || command.id.length > 200) return
176
+ if (typeof command.id !== "string") return
177
177
 
178
178
  const previousAcknowledgement = completedCommands.get(command.id)
179
179
  if (previousAcknowledgement) {
@@ -238,7 +238,7 @@ export function createPermissionController({
238
238
  externalId: command.externalId,
239
239
  processedAt: new Date().toISOString(),
240
240
  }
241
- rememberBounded(completedCommands, command.id, acknowledgement)
241
+ remember(completedCommands, command.id, acknowledgement)
242
242
 
243
243
  if (isQuestion) {
244
244
  reportQuestionAcknowledgement(acknowledgement, pending, reporter, conversation)
package/src/plugin.js CHANGED
@@ -45,10 +45,13 @@ export default async function BackeerPlugin({ client, directory, feedback, v2 =
45
45
  return {}
46
46
  }
47
47
 
48
+ const shutdown = new AbortController()
49
+ config.signal = shutdown.signal
48
50
  const reporter = createOpenCodeReporter(client, config.logLevel)
49
51
  const conversation = feedback ?? createConversationFeedback({ client, reporter })
50
52
  if (config.liveBridge.error) reporter.error("Live control disabled", config.liveBridge.error)
51
- const bridgeConfig = config.liveBridge.owner === "tui"
53
+ const bridgeConfig = config.liveBridge.owner === "tui" ||
54
+ (config.liveBridge.owner === "standalone" && (!v2 || !process.argv.includes("--stdio")))
52
55
  ? { ...config, liveBridge: { enabled: false } } : config
53
56
  const liveBridge = await createLiveBridge({ config: bridgeConfig, client, directory, reporter }).catch((error) => {
54
57
  reporter.error("Live control unavailable", error)
@@ -232,6 +235,7 @@ export default async function BackeerPlugin({ client, directory, feedback, v2 =
232
235
  await Promise.all([handleNotificationEvent(event), liveBridge.handle(event)])
233
236
  },
234
237
  dispose: async () => {
238
+ shutdown.abort()
235
239
  await liveBridge.dispose()
236
240
  await permissionController.dispose()
237
241
  lifecycleController.dispose()
package/src/reporter.js CHANGED
@@ -1,4 +1,4 @@
1
- import { limit } from "./utilities.js"
1
+ import { formatText } from "./utilities.js"
2
2
 
3
3
  const LOG_LEVELS = new Set(["silent", "error", "info", "debug"])
4
4
  const LOG_RANK = { silent: 0, error: 1, info: 2, debug: 3 }
@@ -36,13 +36,10 @@ export function createConsoleReporter(levelValue) {
36
36
  if (previous && now - previous < LOG_DEDUPLICATION_MS) return
37
37
  recentlyEmitted.delete(key)
38
38
  recentlyEmitted.set(key, now)
39
- while (recentlyEmitted.size > 100) {
40
- recentlyEmitted.delete(recentlyEmitted.keys().next().value)
41
- }
42
39
  }
43
40
 
44
41
  const technical = level === "debug" && detail
45
- ? decorate("2", ` · ${limit(detail, 500)}`)
42
+ ? decorate("2", ` · ${formatText(detail)}`)
46
43
  : ""
47
44
  const line = `${symbols[kind]} ${brand} ${message}${technical}`
48
45
  if (kind === "error" || kind === "warning") console.error(line)
@@ -80,10 +77,10 @@ export function createOpenCodeReporter(client, levelValue) {
80
77
  level: kind === "warning" ? "warn" : kind === "applied" || kind === "success"
81
78
  ? "info"
82
79
  : kind,
83
- message: limit(message, 500),
80
+ message: formatText(message),
84
81
  }
85
82
  if (level === "debug" && detail) {
86
- body.extra = { detail: limit(detail?.message ?? detail, 500) }
83
+ body.extra = { detail: formatText(detail?.message ?? detail) }
87
84
  }
88
85
 
89
86
  // Logging during plugin initialization can deadlock older OpenCode builds.
@@ -106,5 +103,4 @@ export function createOpenCodeReporter(client, levelValue) {
106
103
  function rememberRecent(map, key, value) {
107
104
  map.delete(key)
108
105
  map.set(key, value)
109
- while (map.size > 100) map.delete(map.keys().next().value)
110
106
  }
@@ -1,5 +1,5 @@
1
1
  import { FeedbackSessionStore } from "./feedback-session-store.js"
2
- import { limit } from "./utilities.js"
2
+ import { formatText } from "./utilities.js"
3
3
 
4
4
  const FEEDBACK_SYMBOLS = {
5
5
  info: "•",
@@ -82,7 +82,7 @@ export async function sendSessionMessage(client, sessionID, text) {
82
82
 
83
83
  export function formatBackeerFeedback(kind, message) {
84
84
  const symbol = FEEDBACK_SYMBOLS[kind] ?? FEEDBACK_SYMBOLS.info
85
- return `▣ Backeer | ${symbol} ${limit(message, 300)}`
85
+ return `▣ Backeer | ${symbol} ${formatText(message)}`
86
86
  }
87
87
 
88
88
  export function formatBackeerFeedbackBatch(items) {
@@ -93,7 +93,7 @@ export function formatBackeerFeedbackBatch(items) {
93
93
  "▣ Backeer",
94
94
  ...items.map((item) => {
95
95
  const symbol = FEEDBACK_SYMBOLS[item.kind] ?? FEEDBACK_SYMBOLS.info
96
- return `${symbol} ${limit(item.message, 300)}`
96
+ return `${symbol} ${formatText(item.message)}`
97
97
  }),
98
98
  ].join("\n")
99
99
  }
package/src/sse.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { HttpError, isAbortError, sleep } from "./http.js"
2
- import { limit } from "./utilities.js"
2
+ import { formatText } from "./utilities.js"
3
3
 
4
4
  export function parseSseFrame(frame) {
5
5
  const data = []
@@ -87,7 +87,7 @@ async function openCommandStream(config, state, signal, fetchImplementation) {
87
87
  signal,
88
88
  })
89
89
  if (!response.ok) {
90
- const responseBody = limit(await response.text(), 500)
90
+ const responseBody = formatText(await response.text())
91
91
  throw new HttpError(
92
92
  `Backeer command stream failed with status ${response.status}`,
93
93
  response.status,
@@ -123,7 +123,7 @@ async function consumeConnection({
123
123
  function streamErrorState(error, lastLoggedError, reconnectDelay, reporter) {
124
124
  const message = error instanceof HttpError
125
125
  ? `${error.message}: ${error.responseBody}`
126
- : limit(error, 500)
126
+ : formatText(error)
127
127
  if (message !== lastLoggedError) {
128
128
  const authenticationFailure = error instanceof HttpError &&
129
129
  [401, 403].includes(error.status)
@@ -137,7 +137,7 @@ function streamErrorState(error, lastLoggedError, reconnectDelay, reporter) {
137
137
  return {
138
138
  lastLoggedError: message,
139
139
  reconnectDelay: error instanceof HttpError && [401, 403, 404].includes(error.status)
140
- ? 60_000
140
+ ? 3000
141
141
  : reconnectDelay,
142
142
  }
143
143
  }
@@ -156,7 +156,7 @@ export async function runCommandStream(
156
156
  },
157
157
  ) {
158
158
  const state = { lastEventID: "", framesReceived: 0 }
159
- let reconnectDelay = 1000
159
+ let reconnectDelay = 3000
160
160
  let lastLoggedError = ""
161
161
 
162
162
  while (!signal.aborted && hasPending()) {
@@ -176,7 +176,7 @@ export async function runCommandStream(
176
176
  now,
177
177
  stableConnectionMs,
178
178
  onStable: () => {
179
- reconnectDelay = 1000
179
+ reconnectDelay = 3000
180
180
  lastLoggedError = ""
181
181
  },
182
182
  })
@@ -189,7 +189,7 @@ export async function runCommandStream(
189
189
 
190
190
  if (!signal.aborted && hasPending()) {
191
191
  await sleepImplementation(reconnectDelay, signal)
192
- reconnectDelay = Math.min(reconnectDelay * 2, 60_000)
192
+ reconnectDelay = 3000
193
193
  }
194
194
  }
195
195
  }
package/src/updates.js CHANGED
@@ -1,4 +1,4 @@
1
- export const PLUGIN_VERSION = "0.8.1"
1
+ export const PLUGIN_VERSION = "0.10.0"
2
2
 
3
3
  const PACKAGE_NAME = "backeer-opencode"
4
4
  const NPM_REGISTRY_URL = "https://registry.npmjs.org"
package/src/utilities.js CHANGED
@@ -1,7 +1,5 @@
1
- const MAX_TRACKED_ITEMS = 500
2
-
3
- export const limit = (value, length = 1000) =>
4
- String(value ?? "").slice(0, length)
1
+ // Legacy formatting API: normalize text without truncating it.
2
+ export const formatText = value => String(value ?? "")
5
3
 
6
4
  export function enabled(value, fallback = true) {
7
5
  if (typeof value === "boolean") return value
@@ -18,8 +16,7 @@ export function stableHash(value) {
18
16
  return (hash >>> 0).toString(16).padStart(8, "0")
19
17
  }
20
18
 
21
- export function rememberBounded(map, key, value) {
19
+ export function remember(map, key, value) {
22
20
  map.delete(key)
23
21
  map.set(key, value)
24
- while (map.size > MAX_TRACKED_ITEMS) map.delete(map.keys().next().value)
25
22
  }