wechat-bridge-opencode 0.1.2

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.
Files changed (82) hide show
  1. package/.opencode/tools/send-wechat.ts +88 -0
  2. package/LICENSE +21 -0
  3. package/README.md +244 -0
  4. package/dist/bin/wechat-acp.d.ts +14 -0
  5. package/dist/bin/wechat-acp.d.ts.map +1 -0
  6. package/dist/bin/wechat-acp.js +284 -0
  7. package/dist/bin/wechat-acp.js.map +1 -0
  8. package/dist/bin/wechat-opencode.d.ts +14 -0
  9. package/dist/bin/wechat-opencode.d.ts.map +1 -0
  10. package/dist/bin/wechat-opencode.js +284 -0
  11. package/dist/bin/wechat-opencode.js.map +1 -0
  12. package/dist/package.json +60 -0
  13. package/dist/src/acp/agent-manager.d.ts +23 -0
  14. package/dist/src/acp/agent-manager.d.ts.map +1 -0
  15. package/dist/src/acp/agent-manager.js +134 -0
  16. package/dist/src/acp/agent-manager.js.map +1 -0
  17. package/dist/src/acp/client.d.ts +49 -0
  18. package/dist/src/acp/client.d.ts.map +1 -0
  19. package/dist/src/acp/client.js +219 -0
  20. package/dist/src/acp/client.js.map +1 -0
  21. package/dist/src/acp/opencode-sessions.d.ts +17 -0
  22. package/dist/src/acp/opencode-sessions.d.ts.map +1 -0
  23. package/dist/src/acp/opencode-sessions.js +59 -0
  24. package/dist/src/acp/opencode-sessions.js.map +1 -0
  25. package/dist/src/acp/session.d.ts +75 -0
  26. package/dist/src/acp/session.d.ts.map +1 -0
  27. package/dist/src/acp/session.js +273 -0
  28. package/dist/src/acp/session.js.map +1 -0
  29. package/dist/src/acp/workspace-manager.d.ts +48 -0
  30. package/dist/src/acp/workspace-manager.d.ts.map +1 -0
  31. package/dist/src/acp/workspace-manager.js +141 -0
  32. package/dist/src/acp/workspace-manager.js.map +1 -0
  33. package/dist/src/adapter/inbound.d.ts +10 -0
  34. package/dist/src/adapter/inbound.d.ts.map +1 -0
  35. package/dist/src/adapter/inbound.js +149 -0
  36. package/dist/src/adapter/inbound.js.map +1 -0
  37. package/dist/src/adapter/outbound.d.ts +9 -0
  38. package/dist/src/adapter/outbound.d.ts.map +1 -0
  39. package/dist/src/adapter/outbound.js +25 -0
  40. package/dist/src/adapter/outbound.js.map +1 -0
  41. package/dist/src/adapter/workspace-cmd.d.ts +53 -0
  42. package/dist/src/adapter/workspace-cmd.d.ts.map +1 -0
  43. package/dist/src/adapter/workspace-cmd.js +172 -0
  44. package/dist/src/adapter/workspace-cmd.js.map +1 -0
  45. package/dist/src/bridge.d.ts +42 -0
  46. package/dist/src/bridge.d.ts.map +1 -0
  47. package/dist/src/bridge.js +557 -0
  48. package/dist/src/bridge.js.map +1 -0
  49. package/dist/src/config.d.ts +63 -0
  50. package/dist/src/config.d.ts.map +1 -0
  51. package/dist/src/config.js +88 -0
  52. package/dist/src/config.js.map +1 -0
  53. package/dist/src/index.d.ts +7 -0
  54. package/dist/src/index.d.ts.map +1 -0
  55. package/dist/src/index.js +6 -0
  56. package/dist/src/index.js.map +1 -0
  57. package/dist/src/weixin/api.d.ts +50 -0
  58. package/dist/src/weixin/api.d.ts.map +1 -0
  59. package/dist/src/weixin/api.js +87 -0
  60. package/dist/src/weixin/api.js.map +1 -0
  61. package/dist/src/weixin/auth.d.ts +20 -0
  62. package/dist/src/weixin/auth.d.ts.map +1 -0
  63. package/dist/src/weixin/auth.js +88 -0
  64. package/dist/src/weixin/auth.js.map +1 -0
  65. package/dist/src/weixin/media.d.ts +24 -0
  66. package/dist/src/weixin/media.d.ts.map +1 -0
  67. package/dist/src/weixin/media.js +91 -0
  68. package/dist/src/weixin/media.js.map +1 -0
  69. package/dist/src/weixin/monitor.d.ts +16 -0
  70. package/dist/src/weixin/monitor.d.ts.map +1 -0
  71. package/dist/src/weixin/monitor.js +111 -0
  72. package/dist/src/weixin/monitor.js.map +1 -0
  73. package/dist/src/weixin/send.d.ts +32 -0
  74. package/dist/src/weixin/send.d.ts.map +1 -0
  75. package/dist/src/weixin/send.js +193 -0
  76. package/dist/src/weixin/send.js.map +1 -0
  77. package/dist/src/weixin/types.d.ts +150 -0
  78. package/dist/src/weixin/types.d.ts.map +1 -0
  79. package/dist/src/weixin/types.js +33 -0
  80. package/dist/src/weixin/types.js.map +1 -0
  81. package/package.json +60 -0
  82. package/scripts/install-tool.mjs +14 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * ACP Client implementation for WeChat.
3
+ *
4
+ * Implements the acp.Client interface: handles session updates (accumulates
5
+ * text chunks), auto-allows all permission requests, and provides filesystem
6
+ * access for the agent.
7
+ */
8
+ import fs from "node:fs";
9
+ export class WeChatAcpClient {
10
+ chunks = [];
11
+ thoughtChunks = [];
12
+ mediaBlocks = [];
13
+ opts;
14
+ lastTypingAt = 0;
15
+ static TYPING_INTERVAL_MS = 5_000;
16
+ constructor(opts) {
17
+ this.opts = opts;
18
+ }
19
+ updateCallbacks(callbacks) {
20
+ this.opts = {
21
+ ...this.opts,
22
+ sendTyping: callbacks.sendTyping,
23
+ onThoughtFlush: callbacks.onThoughtFlush,
24
+ onMediaFlush: callbacks.onMediaFlush,
25
+ };
26
+ }
27
+ async requestPermission(params) {
28
+ // Auto-allow: find first "allow" option
29
+ const allowOpt = params.options.find((o) => o.kind === "allow_once" || o.kind === "allow_always");
30
+ const optionId = allowOpt?.optionId ?? params.options[0]?.optionId ?? "allow";
31
+ this.opts.log(`[permission] auto-allowed: ${params.toolCall?.title ?? "unknown"} → ${optionId}`);
32
+ return {
33
+ outcome: {
34
+ outcome: "selected",
35
+ optionId,
36
+ },
37
+ };
38
+ }
39
+ async sessionUpdate(params) {
40
+ const update = params.update;
41
+ switch (update.sessionUpdate) {
42
+ case "agent_message_chunk":
43
+ await this.maybeFlushThoughts();
44
+ if (update.content.type === "text") {
45
+ this.chunks.push(update.content.text);
46
+ }
47
+ else if (update.content.type === "image") {
48
+ // Image content - send immediately via media callback
49
+ const imageBlock = {
50
+ type: "image",
51
+ data: update.content.data,
52
+ mimeType: update.content.mimeType,
53
+ };
54
+ await this.flushMedia([imageBlock]);
55
+ }
56
+ else if (update.content.type === "resource") {
57
+ // Resource content - could be text or binary
58
+ const resource = update.content.resource;
59
+ // Check if it's a BlobResourceContents (has blob field)
60
+ if ("blob" in resource && resource.blob != null) {
61
+ // Binary resource
62
+ const resourceBlock = {
63
+ type: "resource",
64
+ uri: resource.uri,
65
+ blob: resource.blob,
66
+ resourceMimeType: resource.mimeType ?? undefined,
67
+ };
68
+ await this.flushMedia([resourceBlock]);
69
+ }
70
+ // Text resources are handled via text chunks in tool_call_update
71
+ }
72
+ // Throttle typing indicators
73
+ await this.maybeSendTyping();
74
+ break;
75
+ case "tool_call":
76
+ await this.maybeFlushThoughts();
77
+ this.opts.log(`[tool] ${update.title} (${update.status})`);
78
+ await this.maybeSendTyping();
79
+ break;
80
+ case "agent_thought_chunk":
81
+ if (update.content.type === "text") {
82
+ const text = update.content.text;
83
+ this.opts.log(`[thought] ${text.length > 80 ? text.substring(0, 80) + "..." : text}`);
84
+ if (this.opts.showThoughts) {
85
+ this.thoughtChunks.push(text);
86
+ }
87
+ }
88
+ await this.maybeSendTyping();
89
+ break;
90
+ case "tool_call_update":
91
+ if (update.status === "completed" && update.content) {
92
+ for (const c of update.content) {
93
+ if (c.type === "diff") {
94
+ const diff = c;
95
+ const header = `--- ${diff.path}`;
96
+ const lines = [header];
97
+ if (diff.oldText != null) {
98
+ for (const l of diff.oldText.split("\n"))
99
+ lines.push(`- ${l}`);
100
+ }
101
+ if (diff.newText != null) {
102
+ for (const l of diff.newText.split("\n"))
103
+ lines.push(`+ ${l}`);
104
+ }
105
+ this.chunks.push("\n```diff\n" + lines.join("\n") + "\n```\n");
106
+ }
107
+ else if (c.type === "content") {
108
+ // Tool result content block - could be text, image, or resource
109
+ const content = c.content;
110
+ if (content.type === "image") {
111
+ const imageBlock = {
112
+ type: "image",
113
+ data: content.data,
114
+ mimeType: content.mimeType,
115
+ };
116
+ await this.flushMedia([imageBlock]);
117
+ }
118
+ else if (content.type === "resource") {
119
+ const resource = content.resource;
120
+ // Check if it's a BlobResourceContents (has blob field)
121
+ if ("blob" in resource && resource.blob != null) {
122
+ const resourceBlock = {
123
+ type: "resource",
124
+ uri: resource.uri,
125
+ blob: resource.blob,
126
+ resourceMimeType: resource.mimeType ?? undefined,
127
+ };
128
+ await this.flushMedia([resourceBlock]);
129
+ }
130
+ }
131
+ // Text content is accumulated to chunks as normal
132
+ }
133
+ }
134
+ }
135
+ if (update.status) {
136
+ this.opts.log(`[tool] ${update.toolCallId} → ${update.status}`);
137
+ }
138
+ break;
139
+ case "plan":
140
+ // Log plan entries
141
+ if (update.entries) {
142
+ const items = update.entries
143
+ .map((e, i) => ` ${i + 1}. [${e.status}] ${e.content}`)
144
+ .join("\n");
145
+ this.opts.log(`[plan]\n${items}`);
146
+ }
147
+ break;
148
+ }
149
+ }
150
+ async readTextFile(params) {
151
+ try {
152
+ const content = await fs.promises.readFile(params.path, "utf-8");
153
+ return { content };
154
+ }
155
+ catch (err) {
156
+ throw new Error(`Failed to read file ${params.path}: ${String(err)}`);
157
+ }
158
+ }
159
+ async writeTextFile(params) {
160
+ try {
161
+ await fs.promises.writeFile(params.path, params.content, "utf-8");
162
+ return {};
163
+ }
164
+ catch (err) {
165
+ throw new Error(`Failed to write file ${params.path}: ${String(err)}`);
166
+ }
167
+ }
168
+ /** Get accumulated text and reset the buffer. Also flushes any remaining thoughts. */
169
+ async flush() {
170
+ await this.maybeFlushThoughts();
171
+ const text = this.chunks.join("");
172
+ this.chunks = [];
173
+ this.lastTypingAt = 0;
174
+ // Also flush any accumulated media blocks
175
+ if (this.mediaBlocks.length > 0) {
176
+ await this.flushMedia(this.mediaBlocks);
177
+ this.mediaBlocks = [];
178
+ }
179
+ return text;
180
+ }
181
+ /** Flush media blocks via callback and reset. */
182
+ async flushMedia(blocks) {
183
+ if (blocks.length === 0)
184
+ return;
185
+ try {
186
+ await this.opts.onMediaFlush(blocks);
187
+ }
188
+ catch {
189
+ // best effort
190
+ }
191
+ }
192
+ async maybeFlushThoughts() {
193
+ if (this.thoughtChunks.length === 0)
194
+ return;
195
+ const thoughtText = this.thoughtChunks.join("");
196
+ this.thoughtChunks = [];
197
+ if (thoughtText.trim()) {
198
+ try {
199
+ await this.opts.onThoughtFlush(`💭 [Thinking]\n${thoughtText}`);
200
+ }
201
+ catch {
202
+ // best effort
203
+ }
204
+ }
205
+ }
206
+ async maybeSendTyping() {
207
+ const now = Date.now();
208
+ if (now - this.lastTypingAt < WeChatAcpClient.TYPING_INTERVAL_MS)
209
+ return;
210
+ this.lastTypingAt = now;
211
+ try {
212
+ await this.opts.sendTyping();
213
+ }
214
+ catch {
215
+ // typing is best-effort
216
+ }
217
+ }
218
+ }
219
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/acp/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Read OpenCode sessions from its SQLite database.
3
+ *
4
+ * Database path:
5
+ * - Linux/macOS: ~/.local/share/opencode/opencode.db
6
+ * - Windows: %USERPROFILE%\.local\share\opencode\opencode.db
7
+ */
8
+ export interface OpencodeSession {
9
+ id: string;
10
+ slug: string;
11
+ projectId: string;
12
+ directory: string;
13
+ title: string;
14
+ timeUpdated: number;
15
+ }
16
+ export declare function listSessions(): OpencodeSession[];
17
+ //# sourceMappingURL=opencode-sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-sessions.d.ts","sourceRoot":"","sources":["../../../src/acp/opencode-sessions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAuBD,wBAAgB,YAAY,IAAI,eAAe,EAAE,CAkBhD"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Read OpenCode sessions from its SQLite database.
3
+ *
4
+ * Database path:
5
+ * - Linux/macOS: ~/.local/share/opencode/opencode.db
6
+ * - Windows: %USERPROFILE%\.local\share\opencode\opencode.db
7
+ */
8
+ import path from "node:path";
9
+ import os from "node:os";
10
+ import fs from "node:fs";
11
+ import { createRequire } from "node:module";
12
+ const require = createRequire(import.meta.url);
13
+ function openDb() {
14
+ const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
15
+ const candidates = [
16
+ path.join(xdgData, "opencode", "opencode.db"),
17
+ path.join(os.homedir(), "Library", "Application Support", "opencode", "opencode.db"),
18
+ path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "opencode", "opencode.db"),
19
+ ];
20
+ let dbPath = null;
21
+ for (const p of candidates) {
22
+ try {
23
+ fs.accessSync(p);
24
+ dbPath = p;
25
+ break;
26
+ }
27
+ catch { /* not found */ }
28
+ }
29
+ if (!dbPath)
30
+ return null;
31
+ try {
32
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
33
+ const Database = require("better-sqlite3");
34
+ return new Database(dbPath, { readonly: true });
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ export function listSessions() {
41
+ const db = openDb();
42
+ if (!db)
43
+ return [];
44
+ try {
45
+ const rows = db.prepare("SELECT id, project_id, slug, directory, title, time_updated FROM session WHERE time_archived IS NULL ORDER BY time_updated DESC").all();
46
+ return rows.map((r) => ({
47
+ id: r.id,
48
+ slug: r.slug,
49
+ projectId: r.project_id,
50
+ directory: r.directory,
51
+ title: r.title,
52
+ timeUpdated: r.time_updated,
53
+ }));
54
+ }
55
+ finally {
56
+ db.close();
57
+ }
58
+ }
59
+ //# sourceMappingURL=opencode-sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-sessions.js","sourceRoot":"","sources":["../../../src/acp/opencode-sessions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAW/C,SAAS,MAAM;IACb,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACxF,MAAM,UAAU,GAAG;QACjB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,aAAa,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,qBAAqB,EAAE,UAAU,EAAE,aAAa,CAAC;QACpF,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,UAAU,EAAE,aAAa,CAAC;KAC3G,CAAC;IACF,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC;YAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAAC,MAAM,GAAG,CAAC,CAAC;YAAC,MAAM;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,CAAC;QACH,iEAAiE;QACjE,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC3C,OAAO,IAAI,QAAQ,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CACrB,iIAAiI,CAClI,CAAC,GAAG,EAAqH,CAAC;QAC3H,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtB,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,SAAS,EAAE,CAAC,CAAC,UAAU;YACvB,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,WAAW,EAAE,CAAC,CAAC,YAAY;SAC5B,CAAC,CAAC,CAAC;IACN,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Per-user ACP session manager.
3
+ *
4
+ * Each WeChat user has at most ONE active agent subprocess at a time.
5
+ * Switching workspace kills the old agent and spawns a new one with the new cwd.
6
+ * Restarting session kills the agent and spawns a new one with the same cwd.
7
+ */
8
+ import type * as acp from "@agentclientprotocol/sdk";
9
+ import { WeChatAcpClient, type MediaContent } from "./client.js";
10
+ import { type AgentProcessInfo } from "./agent-manager.js";
11
+ export interface PendingMessage {
12
+ prompt: acp.ContentBlock[];
13
+ contextToken: string;
14
+ }
15
+ export interface UserSession {
16
+ userId: string;
17
+ contextToken: string;
18
+ client: WeChatAcpClient;
19
+ agentInfo: AgentProcessInfo;
20
+ queue: PendingMessage[];
21
+ processing: boolean;
22
+ lastActivity: number;
23
+ createdAt: number;
24
+ /** Set to true when agent is fully initialized */
25
+ ready: boolean;
26
+ }
27
+ export interface SessionManagerOpts {
28
+ agentCommand: string;
29
+ agentArgs: string[];
30
+ agentEnv?: Record<string, string>;
31
+ idleTimeoutMs: number;
32
+ maxConcurrentUsers: number;
33
+ showThoughts: boolean;
34
+ log: (msg: string) => void;
35
+ onReply: (userId: string, contextToken: string, text: string) => Promise<void>;
36
+ onMediaReply: (userId: string, contextToken: string, blocks: MediaContent[]) => Promise<void>;
37
+ sendTyping: (userId: string, contextToken: string) => Promise<void>;
38
+ /** Resolve cwd for a given userId */
39
+ resolveCwd: (userId: string) => string;
40
+ /** Get existing OpenCode session ID to resume (optional) */
41
+ getExistingSessionId?: (userId: string) => string | undefined;
42
+ /** Called after agent starts with the actual session ID */
43
+ onSessionReady?: (userId: string, sessionId: string) => void;
44
+ }
45
+ export declare class SessionManager {
46
+ private sessions;
47
+ private cleanupTimer;
48
+ private opts;
49
+ private aborted;
50
+ constructor(opts: SessionManagerOpts);
51
+ start(): void;
52
+ stop(): Promise<void>;
53
+ enqueue(userId: string, message: PendingMessage): Promise<void>;
54
+ /**
55
+ * Restart session for a user: kill old agent, spawn new one with current cwd.
56
+ * Clears ACP conversation context.
57
+ */
58
+ restartSession(userId: string, contextToken: string): Promise<UserSession | null>;
59
+ /**
60
+ * Switch workspace: kill old agent and start new one immediately.
61
+ */
62
+ switchWorkspace(userId: string, contextToken: string): Promise<void>;
63
+ private spawnAndReplace;
64
+ getSession(userId: string): UserSession | undefined;
65
+ getUserBySessionId(acpSessionId: string): {
66
+ userId: string;
67
+ contextToken: string;
68
+ } | null;
69
+ get activeCount(): number;
70
+ private createSession;
71
+ private processQueue;
72
+ private cleanupIdleSessions;
73
+ private evictOldest;
74
+ }
75
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/acp/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Per-user ACP session manager.
3
+ *
4
+ * Each WeChat user has at most ONE active agent subprocess at a time.
5
+ * Switching workspace kills the old agent and spawns a new one with the new cwd.
6
+ * Restarting session kills the agent and spawns a new one with the same cwd.
7
+ */
8
+ import { WeChatAcpClient } from "./client.js";
9
+ import { spawnAgent, killAgent } from "./agent-manager.js";
10
+ export class SessionManager {
11
+ sessions = new Map();
12
+ cleanupTimer = null;
13
+ opts;
14
+ aborted = false;
15
+ constructor(opts) {
16
+ this.opts = opts;
17
+ }
18
+ start() {
19
+ this.cleanupTimer = setInterval(() => this.cleanupIdleSessions(), 2 * 60_000);
20
+ this.cleanupTimer.unref();
21
+ }
22
+ async stop() {
23
+ this.aborted = true;
24
+ if (this.cleanupTimer) {
25
+ clearInterval(this.cleanupTimer);
26
+ this.cleanupTimer = null;
27
+ }
28
+ for (const session of this.sessions.values()) {
29
+ killAgent(session.agentInfo.process);
30
+ }
31
+ this.sessions.clear();
32
+ }
33
+ async enqueue(userId, message) {
34
+ let session = this.sessions.get(userId);
35
+ if (!session) {
36
+ if (this.sessions.size >= this.opts.maxConcurrentUsers) {
37
+ this.evictOldest();
38
+ }
39
+ session = await this.createSession(userId, message.contextToken, this.opts.getExistingSessionId?.(userId));
40
+ this.sessions.set(userId, session);
41
+ }
42
+ session.contextToken = message.contextToken;
43
+ session.lastActivity = Date.now();
44
+ session.queue.push(message);
45
+ if (!session.processing) {
46
+ session.processing = true;
47
+ this.processQueue(session).catch((err) => {
48
+ this.opts.log(`[${userId}] queue processing error: ${String(err)}`);
49
+ });
50
+ }
51
+ }
52
+ /**
53
+ * Restart session for a user: kill old agent, spawn new one with current cwd.
54
+ * Clears ACP conversation context.
55
+ */
56
+ async restartSession(userId, contextToken) {
57
+ const oldSession = this.sessions.get(userId);
58
+ if (oldSession) {
59
+ this.opts.log(`[${userId}] Restarting session`);
60
+ killAgent(oldSession.agentInfo.process);
61
+ this.sessions.delete(userId);
62
+ }
63
+ const newSession = await this.createSession(userId, contextToken);
64
+ this.sessions.set(userId, newSession);
65
+ return newSession;
66
+ }
67
+ /**
68
+ * Switch workspace: kill old agent and start new one immediately.
69
+ */
70
+ async switchWorkspace(userId, contextToken) {
71
+ const oldSession = this.sessions.get(userId);
72
+ if (oldSession) {
73
+ this.opts.log(`[${userId}] Switching workspace: killing agent`);
74
+ killAgent(oldSession.agentInfo.process);
75
+ this.sessions.delete(userId);
76
+ }
77
+ const cwd = this.opts.resolveCwd(userId);
78
+ this.opts.log(`Starting new session for ${userId} (cwd: ${cwd})`);
79
+ // Create placeholder session immediately so enqueue sees it and queues messages
80
+ const client = new WeChatAcpClient({
81
+ sendTyping: () => this.opts.sendTyping(userId, contextToken),
82
+ onThoughtFlush: (text) => this.opts.onReply(userId, contextToken, text),
83
+ onMediaFlush: (blocks) => this.opts.onMediaReply(userId, contextToken, blocks),
84
+ log: (msg) => this.opts.log(`[${userId}] ${msg}`),
85
+ showThoughts: this.opts.showThoughts,
86
+ });
87
+ const placeholder = {
88
+ userId,
89
+ contextToken,
90
+ client,
91
+ agentInfo: {
92
+ process: null,
93
+ connection: null,
94
+ sessionId: "",
95
+ },
96
+ queue: [],
97
+ processing: false,
98
+ lastActivity: Date.now(),
99
+ createdAt: Date.now(),
100
+ ready: false,
101
+ };
102
+ this.sessions.set(userId, placeholder);
103
+ // Now spawn the agent in background
104
+ this.spawnAndReplace(userId, contextToken, cwd, client).catch((err) => {
105
+ this.opts.log(`[${userId}] Failed to spawn agent: ${String(err)}`);
106
+ this.sessions.delete(userId);
107
+ });
108
+ }
109
+ async spawnAndReplace(userId, contextToken, cwd, client) {
110
+ const existingSessionId = this.opts.getExistingSessionId?.(userId);
111
+ const agentInfo = await spawnAgent({
112
+ command: this.opts.agentCommand,
113
+ args: this.opts.agentArgs,
114
+ cwd,
115
+ env: this.opts.agentEnv,
116
+ client,
117
+ log: (msg) => this.opts.log(`[${userId}] ${msg}`),
118
+ existingSessionId,
119
+ });
120
+ const session = {
121
+ userId,
122
+ contextToken,
123
+ client,
124
+ agentInfo,
125
+ queue: [],
126
+ processing: false,
127
+ lastActivity: Date.now(),
128
+ createdAt: Date.now(),
129
+ ready: true,
130
+ };
131
+ agentInfo.process.on("exit", () => {
132
+ const s = this.sessions.get(userId);
133
+ if (s && s.agentInfo.process === agentInfo.process) {
134
+ this.opts.log(`Agent process for ${userId} exited, removing session`);
135
+ this.sessions.delete(userId);
136
+ }
137
+ });
138
+ this.opts.onSessionReady?.(userId, agentInfo.sessionId);
139
+ this.sessions.set(userId, session);
140
+ }
141
+ getSession(userId) {
142
+ return this.sessions.get(userId);
143
+ }
144
+ getUserBySessionId(acpSessionId) {
145
+ for (const [userId, session] of this.sessions) {
146
+ if (session.agentInfo.sessionId === acpSessionId) {
147
+ return { userId, contextToken: session.contextToken };
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+ get activeCount() {
153
+ return this.sessions.size;
154
+ }
155
+ async createSession(userId, contextToken, existingSessionId) {
156
+ const cwd = this.opts.resolveCwd(userId);
157
+ this.opts.log(`Creating new session for ${userId} (cwd: ${cwd}${existingSessionId ? `, resume: ${existingSessionId}` : ""})`);
158
+ const client = new WeChatAcpClient({
159
+ sendTyping: () => this.opts.sendTyping(userId, contextToken),
160
+ onThoughtFlush: (text) => this.opts.onReply(userId, contextToken, text),
161
+ onMediaFlush: (blocks) => this.opts.onMediaReply(userId, contextToken, blocks),
162
+ log: (msg) => this.opts.log(`[${userId}] ${msg}`),
163
+ showThoughts: this.opts.showThoughts,
164
+ });
165
+ const agentInfo = await spawnAgent({
166
+ command: this.opts.agentCommand,
167
+ args: this.opts.agentArgs,
168
+ cwd,
169
+ env: this.opts.agentEnv,
170
+ client,
171
+ log: (msg) => this.opts.log(`[${userId}] ${msg}`),
172
+ existingSessionId,
173
+ });
174
+ agentInfo.process.on("exit", () => {
175
+ const s = this.sessions.get(userId);
176
+ if (s && s.agentInfo.process === agentInfo.process) {
177
+ this.opts.log(`Agent process for ${userId} exited, removing session`);
178
+ this.sessions.delete(userId);
179
+ }
180
+ });
181
+ // Notify bridge of the actual session ID
182
+ this.opts.onSessionReady?.(userId, agentInfo.sessionId);
183
+ return {
184
+ userId,
185
+ contextToken,
186
+ client,
187
+ agentInfo,
188
+ queue: [],
189
+ processing: false,
190
+ lastActivity: Date.now(),
191
+ createdAt: Date.now(),
192
+ ready: true,
193
+ };
194
+ }
195
+ async processQueue(session) {
196
+ try {
197
+ while (session.queue.length > 0 && !this.aborted) {
198
+ const pending = session.queue.shift();
199
+ session.client.updateCallbacks({
200
+ sendTyping: () => this.opts.sendTyping(session.userId, pending.contextToken),
201
+ onThoughtFlush: (text) => this.opts.onReply(session.userId, pending.contextToken, text),
202
+ onMediaFlush: (blocks) => this.opts.onMediaReply(session.userId, pending.contextToken, blocks),
203
+ });
204
+ await session.client.flush();
205
+ try {
206
+ this.opts.sendTyping(session.userId, pending.contextToken).catch(() => { });
207
+ this.opts.log(`[${session.userId}] Sending prompt to agent...`);
208
+ const result = await session.agentInfo.connection.prompt({
209
+ sessionId: session.agentInfo.sessionId,
210
+ prompt: pending.prompt,
211
+ });
212
+ let replyText = await session.client.flush();
213
+ if (result.stopReason === "cancelled") {
214
+ replyText += "\n[cancelled]";
215
+ }
216
+ else if (result.stopReason === "refusal") {
217
+ replyText += "\n[agent refused to continue]";
218
+ }
219
+ this.opts.log(`[${session.userId}] Agent done (${result.stopReason}), reply ${replyText.length} chars`);
220
+ if (replyText.trim()) {
221
+ await this.opts.onReply(session.userId, pending.contextToken, replyText);
222
+ }
223
+ }
224
+ catch (err) {
225
+ this.opts.log(`[${session.userId}] Agent prompt error: ${String(err)}`);
226
+ if (session.agentInfo.process.killed || session.agentInfo.process.exitCode !== null) {
227
+ this.opts.log(`[${session.userId}] Agent process died, removing session`);
228
+ this.sessions.delete(session.userId);
229
+ return;
230
+ }
231
+ try {
232
+ await this.opts.onReply(session.userId, pending.contextToken, `⚠️ Agent error: ${String(err)}`);
233
+ }
234
+ catch {
235
+ // best effort
236
+ }
237
+ }
238
+ }
239
+ }
240
+ finally {
241
+ session.processing = false;
242
+ }
243
+ }
244
+ cleanupIdleSessions() {
245
+ if (this.opts.idleTimeoutMs <= 0) {
246
+ return;
247
+ }
248
+ const now = Date.now();
249
+ for (const [userId, session] of this.sessions) {
250
+ if (now - session.lastActivity > this.opts.idleTimeoutMs && !session.processing) {
251
+ this.opts.log(`Session for ${userId} idle for ${Math.round((now - session.lastActivity) / 60_000)}min, removing`);
252
+ killAgent(session.agentInfo.process);
253
+ this.sessions.delete(userId);
254
+ }
255
+ }
256
+ }
257
+ evictOldest() {
258
+ let oldest = null;
259
+ for (const [userId, session] of this.sessions) {
260
+ if (!session.processing && (!oldest || session.lastActivity < oldest.lastActivity)) {
261
+ oldest = { userId, lastActivity: session.lastActivity };
262
+ }
263
+ }
264
+ if (oldest) {
265
+ this.opts.log(`Evicting oldest idle session: ${oldest.userId}`);
266
+ const session = this.sessions.get(oldest.userId);
267
+ if (session)
268
+ killAgent(session.agentInfo.process);
269
+ this.sessions.delete(oldest.userId);
270
+ }
271
+ }
272
+ }
273
+ //# sourceMappingURL=session.js.map