Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/bus-publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export class BusPublisher {
private lastPublish = 0;
private publishInterval = 500; // ms — debounce
private reconnecting = false;
private onWarn: ((message: string, ...args: unknown[]) => void) | undefined;

async init(): Promise<void> {
async init(opts?: { onWarn?: (message: string, ...args: unknown[]) => void }): Promise<void> {
this.onWarn = opts?.onWarn;
try {
this.bus = await BusClient.connect({ timeoutMs: 3000 });
this.bus = await BusClient.connect({ timeoutMs: 3000, onWarn: opts?.onWarn });
} catch {
this.bus = null;
}
Expand Down Expand Up @@ -53,7 +55,7 @@ export class BusPublisher {
this.reconnecting = true;
setTimeout(async () => {
this.reconnecting = false;
await this.init();
await this.init({ onWarn: this.onWarn });
}, 5000);
Comment on lines 56 to 59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reconnect attempts can stop permanently after a failed retry.

Line 57 resets this.reconnecting before the reconnect attempt finishes. If init fails at Line 58, this.bus remains null and publish exits early at Line 27, so no further retries are scheduled.

Proposed fix
-        setTimeout(async () => {
-          this.reconnecting = false;
-          await this.init({ onWarn: this.onWarn });
-        }, 5000);
+        const reconnect = async (): Promise<void> => {
+          await this.init({ onWarn: this.onWarn });
+          if (!this.bus) {
+            setTimeout(reconnect, 5000);
+            return;
+          }
+          this.reconnecting = false;
+        };
+        setTimeout(reconnect, 5000);
Committable suggestion

IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout(async () => {
this.reconnecting = false;
await this.init();
await this.init({ onWarn: this.onWarn });
}, 5000);
const reconnect = async (): Promise<void> => {
await this.init({ onWarn: this.onWarn });
if (!this.bus) {
setTimeout(reconnect, 5000);
return;
}
this.reconnecting = false;
};
setTimeout(reconnect, 5000);
Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bus-publisher.ts` around lines 56 - 59, The reconnection logic in the
setTimeout callback sets this.reconnecting to false before the reconnect attempt
completes, causing permanent disconnection if init fails. Wrap the await
this.init call in a try-catch block and only set this.reconnecting to false
after the init call succeeds. If init fails in the catch block, handle the error
appropriately (such as logging it) to allow subsequent reconnection attempts to
be scheduled properly.

}
}
Expand Down
15 changes: 14 additions & 1 deletion src/four-opencode-token-budget-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,26 @@ const sessionTokens = new SessionTokenCache(

// ── Plugin Bus (P4d) ────────────────────────────────────
const busPublisher = new BusPublisher();
busPublisher.init(); // fire-and-forget — connects if bus available
// init() is called inside the Plugin callback once ctx.client is available

// Track current session ID (set on first event)
let currentSessionID = "";

export const FourTokenBudgetGuardPlugin: Plugin = async (ctx) => {
const config = loadConfig();
// Initialize bus publisher with app.log for warning messages
await busPublisher.init({
onWarn: (msg, ...args) => {
ctx.client.app.log({
body: {
service: "tbg",
level: "warn",
message: msg,
extra: { details: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(" ") }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: JSON.stringify(a) throws on circular objects. If a warning arg is circular, the entire onWarn callback throws, potentially breaking BusClient's internal warning handler.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/four-opencode-token-budget-guard.ts, line 40:

<comment>`JSON.stringify(a)` throws on circular objects. If a warning arg is circular, the entire `onWarn` callback throws, potentially breaking BusClient's internal warning handler.</comment>

<file context>
@@ -30,14 +30,14 @@ let currentSessionID = "";
           level: "warn",
           message: msg,
-          extra: { details: args.map(String).join(" ") }
+          extra: { details: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(" ") }
         }
       }).catch(() => {});
</file context>
Suggested change
extra: { details: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(" ") }
extra: { details: args.map(a => { try { return typeof a === 'object' ? JSON.stringify(a) : String(a); } catch { return String(a); } }).join(" ") }

}
}).catch(() => {});
}
});
logDebugEvent("plugin.loaded", { directory: ctx.directory });
const policyConfig = loadPolicyConfig();
const policies: Policy[] = [new MaxStartTokensPolicy()];
Expand Down
Loading