opencode-copilot-account-switcher 0.14.22 → 0.14.24

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.
@@ -51,6 +51,7 @@ export type WechatBridgeInput = {
51
51
  directory: string;
52
52
  client: WechatBridgeClient;
53
53
  liveReadTimeoutMs?: number;
54
+ onDiagnosticEvent?: (event: WechatBridgeDiagnosticEvent) => Promise<void> | void;
54
55
  };
55
56
  export type WechatBridge = {
56
57
  collectStatusSnapshot: () => Promise<WechatInstanceStatusSnapshot>;
@@ -75,6 +76,21 @@ type WechatBridgeLifecycleDeps = {
75
76
  setIntervalImpl?: typeof setInterval;
76
77
  clearIntervalImpl?: typeof clearInterval;
77
78
  };
79
+ type WechatBridgeDiagnosticEvent = {
80
+ type: "collectStatusStage";
81
+ instanceID: string;
82
+ stage: string;
83
+ status: "fulfilled" | "rejected";
84
+ durationMs: number;
85
+ timeout?: boolean;
86
+ error?: string;
87
+ } | {
88
+ type: "collectStatusCompleted";
89
+ instanceID: string;
90
+ durationMs: number;
91
+ sessionCount: number;
92
+ unavailable?: InstanceUnavailableKind[];
93
+ };
78
94
  export declare function createWechatBridge(input: WechatBridgeInput): WechatBridge;
79
95
  export declare function createWechatBridgeLifecycle(input: WechatBridgeLifecycleInput, deps?: WechatBridgeLifecycleDeps): Promise<WechatBridgeLifecycle>;
80
96
  export {};
@@ -1,5 +1,8 @@
1
+ import path from "node:path";
2
+ import { appendFile, mkdir } from "node:fs/promises";
1
3
  import { connect } from "./broker-client.js";
2
4
  import { connectOrSpawnBroker } from "./broker-launcher.js";
5
+ import { WECHAT_FILE_MODE, wechatBridgeDiagnosticsPath } from "./state-paths.js";
3
6
  import { buildSessionDigest, groupPermissionsBySession, groupQuestionsBySession, pickRecentSessions, } from "./session-digest.js";
4
7
  const DEFAULT_LIVE_READ_TIMEOUT_MS = 2_000;
5
8
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;
@@ -66,6 +69,61 @@ function withTimeout(task, timeoutMs, name) {
66
69
  });
67
70
  });
68
71
  }
72
+ function isErrorWithMessage(value) {
73
+ return typeof value === "object" && value !== null && "message" in value && typeof value.message === "string";
74
+ }
75
+ function createWechatBridgeDiagnosticsWriter(filePath = wechatBridgeDiagnosticsPath()) {
76
+ let warned = false;
77
+ return async (event) => {
78
+ try {
79
+ await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
80
+ const line = `${JSON.stringify({ timestamp: Date.now(), ...event })}\n`;
81
+ await appendFile(filePath, line, { encoding: "utf8", mode: WECHAT_FILE_MODE });
82
+ }
83
+ catch (error) {
84
+ if (!warned) {
85
+ warned = true;
86
+ console.warn("[wechat-bridge] failed to write diagnostics", error);
87
+ }
88
+ }
89
+ };
90
+ }
91
+ function isTimeoutError(error) {
92
+ return isErrorWithMessage(error) && /timed out/i.test(error.message);
93
+ }
94
+ function toDiagnosticErrorMessage(error) {
95
+ if (isErrorWithMessage(error)) {
96
+ return error.message;
97
+ }
98
+ return String(error);
99
+ }
100
+ function wrapDiagnosticStage(input, task) {
101
+ const startedAt = Date.now();
102
+ return Promise.resolve()
103
+ .then(task)
104
+ .then((value) => {
105
+ void Promise.resolve(input.onDiagnosticEvent?.({
106
+ type: "collectStatusStage",
107
+ instanceID: input.instanceID,
108
+ stage: input.stage,
109
+ status: "fulfilled",
110
+ durationMs: Date.now() - startedAt,
111
+ })).catch(() => { });
112
+ return value;
113
+ })
114
+ .catch((error) => {
115
+ void Promise.resolve(input.onDiagnosticEvent?.({
116
+ type: "collectStatusStage",
117
+ instanceID: input.instanceID,
118
+ stage: input.stage,
119
+ status: "rejected",
120
+ durationMs: Date.now() - startedAt,
121
+ timeout: isTimeoutError(error),
122
+ error: toDiagnosticErrorMessage(error),
123
+ })).catch(() => { });
124
+ throw error;
125
+ });
126
+ }
69
127
  function isSdkFieldsResult(value) {
70
128
  return typeof value === "object"
71
129
  && value !== null
@@ -85,15 +143,17 @@ function unwrapSdkReadResult(value, name) {
85
143
  }
86
144
  export function createWechatBridge(input) {
87
145
  const collectStatusSnapshot = async () => {
146
+ const startedAt = Date.now();
88
147
  const liveReadTimeoutMs = typeof input.liveReadTimeoutMs === "number" && Number.isFinite(input.liveReadTimeoutMs)
89
148
  ? Math.max(1, Math.floor(input.liveReadTimeoutMs))
90
149
  : DEFAULT_LIVE_READ_TIMEOUT_MS;
91
150
  const unavailable = new Set();
151
+ const onDiagnosticEvent = input.onDiagnosticEvent;
92
152
  const [sessionListResult, statusResult, questionResult, permissionResult] = await Promise.allSettled([
93
- withTimeout(async () => unwrapSdkReadResult(await input.client.session.list(), "session.list"), liveReadTimeoutMs, "session.list"),
94
- withTimeout(async () => unwrapSdkReadResult(await input.client.session.status(), "session.status"), liveReadTimeoutMs, "session.status"),
95
- withTimeout(async () => unwrapSdkReadResult(await input.client.question.list(), "question.list"), liveReadTimeoutMs, "question.list"),
96
- withTimeout(async () => unwrapSdkReadResult(await input.client.permission.list(), "permission.list"), liveReadTimeoutMs, "permission.list"),
153
+ wrapDiagnosticStage({ instanceID: input.instanceID, stage: "session.list", onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.session.list(), "session.list"), liveReadTimeoutMs, "session.list")),
154
+ wrapDiagnosticStage({ instanceID: input.instanceID, stage: "session.status", onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.session.status(), "session.status"), liveReadTimeoutMs, "session.status")),
155
+ wrapDiagnosticStage({ instanceID: input.instanceID, stage: "question.list", onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.question.list(), "question.list"), liveReadTimeoutMs, "question.list")),
156
+ wrapDiagnosticStage({ instanceID: input.instanceID, stage: "permission.list", onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.permission.list(), "permission.list"), liveReadTimeoutMs, "permission.list")),
97
157
  ]);
98
158
  const sessions = sessionListResult.status === "fulfilled" ? sessionListResult.value : [];
99
159
  const recentSessions = pickRecentSessions(sessions, 3);
@@ -111,8 +171,8 @@ export function createWechatBridge(input) {
111
171
  : (unavailable.add("permissionList"), groupPermissionsBySession([]));
112
172
  const sessionDigests = await Promise.all(recentSessions.map(async (session) => {
113
173
  const [todoResult, messagesResult] = await Promise.allSettled([
114
- withTimeout(async () => unwrapSdkReadResult(await input.client.session.todo({ sessionID: session.id }), `session.todo:${session.id}`), liveReadTimeoutMs, `session.todo:${session.id}`),
115
- withTimeout(async () => unwrapSdkReadResult(await input.client.session.messages({ sessionID: session.id }), `session.messages:${session.id}`), liveReadTimeoutMs, `session.messages:${session.id}`),
174
+ wrapDiagnosticStage({ instanceID: input.instanceID, stage: `session.todo:${session.id}`, onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.session.todo({ sessionID: session.id }), `session.todo:${session.id}`), liveReadTimeoutMs, `session.todo:${session.id}`)),
175
+ wrapDiagnosticStage({ instanceID: input.instanceID, stage: `session.messages:${session.id}`, onDiagnosticEvent }, () => withTimeout(async () => unwrapSdkReadResult(await input.client.session.messages({ sessionID: session.id }), `session.messages:${session.id}`), liveReadTimeoutMs, `session.messages:${session.id}`)),
116
176
  ]);
117
177
  const sessionUnavailable = [];
118
178
  const todos = todoResult.status === "fulfilled" ? todoResult.value : (sessionUnavailable.push("todo"), []);
@@ -129,7 +189,7 @@ export function createWechatBridge(input) {
129
189
  unavailable: sessionUnavailable,
130
190
  });
131
191
  }));
132
- return {
192
+ const snapshot = {
133
193
  instanceID: input.instanceID,
134
194
  instanceName: input.instanceName,
135
195
  pid: input.pid,
@@ -139,6 +199,14 @@ export function createWechatBridge(input) {
139
199
  sessions: sessionDigests,
140
200
  unavailable: unavailable.size > 0 ? [...unavailable] : undefined,
141
201
  };
202
+ void Promise.resolve(onDiagnosticEvent?.({
203
+ type: "collectStatusCompleted",
204
+ instanceID: input.instanceID,
205
+ durationMs: Date.now() - startedAt,
206
+ sessionCount: snapshot.sessions.length,
207
+ unavailable: snapshot.unavailable,
208
+ })).catch(() => { });
209
+ return snapshot;
142
210
  };
143
211
  return {
144
212
  collectStatusSnapshot,
@@ -164,6 +232,7 @@ export async function createWechatBridgeLifecycle(input, deps = {}) {
164
232
  projectName,
165
233
  directory,
166
234
  client: input.client,
235
+ onDiagnosticEvent: createWechatBridgeDiagnosticsWriter(),
167
236
  });
168
237
  const broker = await connectOrSpawnBrokerImpl();
169
238
  const brokerClient = await connectImpl(broker.endpoint, { bridge });
@@ -1,6 +1,6 @@
1
1
  import type { WechatSlashCommand } from "./command-parser.js";
2
2
  export declare const DEFAULT_HEARTBEAT_TIMEOUT_MS = 30000;
3
- export declare const DEFAULT_STATUS_COLLECT_WINDOW_MS = 1500;
3
+ export declare const DEFAULT_STATUS_COLLECT_WINDOW_MS = 5000;
4
4
  type AggregatedStatusInstance = {
5
5
  instanceID: string;
6
6
  status: "ok";
@@ -15,7 +15,7 @@ const FUTURE_MESSAGE_TYPES = new Set([
15
15
  ]);
16
16
  export const DEFAULT_HEARTBEAT_TIMEOUT_MS = 30_000;
17
17
  const DEFAULT_HEARTBEAT_SCAN_INTERVAL_MS = 1_000;
18
- export const DEFAULT_STATUS_COLLECT_WINDOW_MS = 1_500;
18
+ export const DEFAULT_STATUS_COLLECT_WINDOW_MS = 5_000;
19
19
  function isNonEmptyString(value) {
20
20
  return typeof value === "string" && value.trim().length > 0;
21
21
  }
@@ -409,6 +409,7 @@ export async function startBrokerServer(endpoint) {
409
409
  await prepareEndpoint(endpoint);
410
410
  const heartbeatTimeoutMs = toPositiveNumber(process.env.WECHAT_BROKER_HEARTBEAT_TIMEOUT_MS, DEFAULT_HEARTBEAT_TIMEOUT_MS);
411
411
  const heartbeatScanIntervalMs = toPositiveNumber(process.env.WECHAT_BROKER_HEARTBEAT_SCAN_INTERVAL_MS, DEFAULT_HEARTBEAT_SCAN_INTERVAL_MS);
412
+ const statusCollectWindowMs = toPositiveNumber(process.env.WECHAT_BROKER_STATUS_COLLECT_WINDOW_MS, DEFAULT_STATUS_COLLECT_WINDOW_MS);
412
413
  const server = net.createServer((socket) => {
413
414
  let buffer = "";
414
415
  let messageChain = Promise.resolve();
@@ -487,7 +488,7 @@ export async function startBrokerServer(endpoint) {
487
488
  return new Promise((resolve) => {
488
489
  const timer = setTimeout(() => {
489
490
  finalizePendingCollectStatus(requestId);
490
- }, DEFAULT_STATUS_COLLECT_WINDOW_MS);
491
+ }, statusCollectWindowMs);
491
492
  pendingCollectStatusByRequestId.set(requestId, {
492
493
  requestedInstanceIDs,
493
494
  snapshotsByInstanceID: new Map(),
@@ -5,6 +5,7 @@ export declare function wechatStateRoot(): string;
5
5
  export declare function brokerStatePath(): string;
6
6
  export declare function wechatStatusRuntimeDiagnosticsPath(stateRoot?: string): string;
7
7
  export declare function brokerStartupDiagnosticsPath(stateRoot?: string): string;
8
+ export declare function wechatBridgeDiagnosticsPath(stateRoot?: string): string;
8
9
  export declare function launchLockPath(): string;
9
10
  export declare function operatorStatePath(): string;
10
11
  export declare function instancesDir(): string;
@@ -15,6 +15,9 @@ export function wechatStatusRuntimeDiagnosticsPath(stateRoot = wechatStateRoot()
15
15
  export function brokerStartupDiagnosticsPath(stateRoot = wechatStateRoot()) {
16
16
  return path.join(stateRoot, "broker-startup.diagnostics.log");
17
17
  }
18
+ export function wechatBridgeDiagnosticsPath(stateRoot = wechatStateRoot()) {
19
+ return path.join(stateRoot, "wechat-bridge.diagnostics.jsonl");
20
+ }
18
21
  export function launchLockPath() {
19
22
  return path.join(wechatStateRoot(), "launch.lock");
20
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.14.22",
3
+ "version": "0.14.24",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",