twinny 0.0.0-dev.260525150705

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 (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/README.zh-CN.md +252 -0
  4. package/configs/banner.png +0 -0
  5. package/configs/logo.png +0 -0
  6. package/dist/app/caffeinate.d.ts +28 -0
  7. package/dist/app/caffeinate.js +96 -0
  8. package/dist/app/caffeinate.js.map +1 -0
  9. package/dist/app/daemon.d.ts +1 -0
  10. package/dist/app/daemon.js +44 -0
  11. package/dist/app/daemon.js.map +1 -0
  12. package/dist/app/lark-assets.d.ts +25 -0
  13. package/dist/app/lark-assets.js +108 -0
  14. package/dist/app/lark-assets.js.map +1 -0
  15. package/dist/app/startup-probe.d.ts +17 -0
  16. package/dist/app/startup-probe.js +90 -0
  17. package/dist/app/startup-probe.js.map +1 -0
  18. package/dist/app/wiring.d.ts +122 -0
  19. package/dist/app/wiring.js +694 -0
  20. package/dist/app/wiring.js.map +1 -0
  21. package/dist/cli/commands.d.ts +1 -0
  22. package/dist/cli/commands.js +47 -0
  23. package/dist/cli/commands.js.map +1 -0
  24. package/dist/cli/install-wizard.d.ts +41 -0
  25. package/dist/cli/install-wizard.js +629 -0
  26. package/dist/cli/install-wizard.js.map +1 -0
  27. package/dist/codex/appserver.d.ts +109 -0
  28. package/dist/codex/appserver.js +308 -0
  29. package/dist/codex/appserver.js.map +1 -0
  30. package/dist/codex/goal.d.ts +64 -0
  31. package/dist/codex/goal.js +433 -0
  32. package/dist/codex/goal.js.map +1 -0
  33. package/dist/codex/index.d.ts +6 -0
  34. package/dist/codex/index.js +7 -0
  35. package/dist/codex/index.js.map +1 -0
  36. package/dist/codex/protocol.d.ts +95 -0
  37. package/dist/codex/protocol.js +205 -0
  38. package/dist/codex/protocol.js.map +1 -0
  39. package/dist/codex/thread-name.d.ts +3 -0
  40. package/dist/codex/thread-name.js +27 -0
  41. package/dist/codex/thread-name.js.map +1 -0
  42. package/dist/codex/thread.d.ts +76 -0
  43. package/dist/codex/thread.js +80 -0
  44. package/dist/codex/thread.js.map +1 -0
  45. package/dist/codex/turn.d.ts +166 -0
  46. package/dist/codex/turn.js +746 -0
  47. package/dist/codex/turn.js.map +1 -0
  48. package/dist/config/bootstrap.d.ts +14 -0
  49. package/dist/config/bootstrap.js +56 -0
  50. package/dist/config/bootstrap.js.map +1 -0
  51. package/dist/config/index.d.ts +4 -0
  52. package/dist/config/index.js +5 -0
  53. package/dist/config/index.js.map +1 -0
  54. package/dist/config/loader.d.ts +49 -0
  55. package/dist/config/loader.js +467 -0
  56. package/dist/config/loader.js.map +1 -0
  57. package/dist/config/paths.d.ts +11 -0
  58. package/dist/config/paths.js +43 -0
  59. package/dist/config/paths.js.map +1 -0
  60. package/dist/config/secrets.d.ts +33 -0
  61. package/dist/config/secrets.js +85 -0
  62. package/dist/config/secrets.js.map +1 -0
  63. package/dist/conversation/manager.d.ts +701 -0
  64. package/dist/conversation/manager.js +7673 -0
  65. package/dist/conversation/manager.js.map +1 -0
  66. package/dist/conversation/queue.d.ts +8 -0
  67. package/dist/conversation/queue.js +28 -0
  68. package/dist/conversation/queue.js.map +1 -0
  69. package/dist/conversation/routing.d.ts +11 -0
  70. package/dist/conversation/routing.js +55 -0
  71. package/dist/conversation/routing.js.map +1 -0
  72. package/dist/errors.d.ts +6 -0
  73. package/dist/errors.js +17 -0
  74. package/dist/errors.js.map +1 -0
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.js +4 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/lark/auth.d.ts +41 -0
  79. package/dist/lark/auth.js +132 -0
  80. package/dist/lark/auth.js.map +1 -0
  81. package/dist/lark/browser-auth.d.ts +68 -0
  82. package/dist/lark/browser-auth.js +258 -0
  83. package/dist/lark/browser-auth.js.map +1 -0
  84. package/dist/lark/cards.d.ts +140 -0
  85. package/dist/lark/cards.js +1150 -0
  86. package/dist/lark/cards.js.map +1 -0
  87. package/dist/lark/contact.d.ts +41 -0
  88. package/dist/lark/contact.js +122 -0
  89. package/dist/lark/contact.js.map +1 -0
  90. package/dist/lark/events.d.ts +65 -0
  91. package/dist/lark/events.js +218 -0
  92. package/dist/lark/events.js.map +1 -0
  93. package/dist/lark/files.d.ts +36 -0
  94. package/dist/lark/files.js +191 -0
  95. package/dist/lark/files.js.map +1 -0
  96. package/dist/lark/filters.d.ts +73 -0
  97. package/dist/lark/filters.js +678 -0
  98. package/dist/lark/filters.js.map +1 -0
  99. package/dist/lark/index.d.ts +10 -0
  100. package/dist/lark/index.js +11 -0
  101. package/dist/lark/index.js.map +1 -0
  102. package/dist/lark/messages.d.ts +87 -0
  103. package/dist/lark/messages.js +428 -0
  104. package/dist/lark/messages.js.map +1 -0
  105. package/dist/lark/openapi.d.ts +58 -0
  106. package/dist/lark/openapi.js +206 -0
  107. package/dist/lark/openapi.js.map +1 -0
  108. package/dist/lark/redactor.d.ts +5 -0
  109. package/dist/lark/redactor.js +68 -0
  110. package/dist/lark/redactor.js.map +1 -0
  111. package/dist/lark/types.d.ts +49 -0
  112. package/dist/lark/types.js +18 -0
  113. package/dist/lark/types.js.map +1 -0
  114. package/dist/launchd/install.d.ts +22 -0
  115. package/dist/launchd/install.js +114 -0
  116. package/dist/launchd/install.js.map +1 -0
  117. package/dist/launchd/plist.d.ts +10 -0
  118. package/dist/launchd/plist.js +61 -0
  119. package/dist/launchd/plist.js.map +1 -0
  120. package/dist/lock/index.d.ts +20 -0
  121. package/dist/lock/index.js +74 -0
  122. package/dist/lock/index.js.map +1 -0
  123. package/dist/main.d.ts +2 -0
  124. package/dist/main.js +11 -0
  125. package/dist/main.js.map +1 -0
  126. package/dist/markdown.d.ts +12 -0
  127. package/dist/markdown.js +149 -0
  128. package/dist/markdown.js.map +1 -0
  129. package/dist/observability/health.d.ts +25 -0
  130. package/dist/observability/health.js +187 -0
  131. package/dist/observability/health.js.map +1 -0
  132. package/dist/observability/logs.d.ts +10 -0
  133. package/dist/observability/logs.js +34 -0
  134. package/dist/observability/logs.js.map +1 -0
  135. package/dist/observability/system-notifications.d.ts +25 -0
  136. package/dist/observability/system-notifications.js +33 -0
  137. package/dist/observability/system-notifications.js.map +1 -0
  138. package/dist/profiles/guest.d.ts +19 -0
  139. package/dist/profiles/guest.js +241 -0
  140. package/dist/profiles/guest.js.map +1 -0
  141. package/dist/profiles/index.d.ts +5 -0
  142. package/dist/profiles/index.js +14 -0
  143. package/dist/profiles/index.js.map +1 -0
  144. package/dist/profiles/owner.d.ts +1 -0
  145. package/dist/profiles/owner.js +6 -0
  146. package/dist/profiles/owner.js.map +1 -0
  147. package/dist/store/db.d.ts +10 -0
  148. package/dist/store/db.js +29 -0
  149. package/dist/store/db.js.map +1 -0
  150. package/dist/store/index.d.ts +3 -0
  151. package/dist/store/index.js +4 -0
  152. package/dist/store/index.js.map +1 -0
  153. package/dist/store/migrations.d.ts +13 -0
  154. package/dist/store/migrations.js +79 -0
  155. package/dist/store/migrations.js.map +1 -0
  156. package/dist/store/repositories.d.ts +227 -0
  157. package/dist/store/repositories.js +1384 -0
  158. package/dist/store/repositories.js.map +1 -0
  159. package/dist/telemetry/client.d.ts +60 -0
  160. package/dist/telemetry/client.js +204 -0
  161. package/dist/telemetry/client.js.map +1 -0
  162. package/dist/telemetry/hash.d.ts +1 -0
  163. package/dist/telemetry/hash.js +8 -0
  164. package/dist/telemetry/hash.js.map +1 -0
  165. package/dist/telemetry/index.d.ts +4 -0
  166. package/dist/telemetry/index.js +5 -0
  167. package/dist/telemetry/index.js.map +1 -0
  168. package/dist/telemetry/posthog.d.ts +27 -0
  169. package/dist/telemetry/posthog.js +45 -0
  170. package/dist/telemetry/posthog.js.map +1 -0
  171. package/dist/telemetry/reporter.d.ts +13 -0
  172. package/dist/telemetry/reporter.js +29 -0
  173. package/dist/telemetry/reporter.js.map +1 -0
  174. package/dist/types.d.ts +330 -0
  175. package/dist/types.js +9 -0
  176. package/dist/types.js.map +1 -0
  177. package/dist/version.d.ts +1 -0
  178. package/dist/version.js +1 -0
  179. package/dist/version.js.map +1 -0
  180. package/dist/version.json +3 -0
  181. package/dist/workspace/index.d.ts +2 -0
  182. package/dist/workspace/index.js +3 -0
  183. package/dist/workspace/index.js.map +1 -0
  184. package/dist/workspace/manager.d.ts +14 -0
  185. package/dist/workspace/manager.js +69 -0
  186. package/dist/workspace/manager.js.map +1 -0
  187. package/dist/workspace/slug.d.ts +8 -0
  188. package/dist/workspace/slug.js +59 -0
  189. package/dist/workspace/slug.js.map +1 -0
  190. package/migrations/0001_initial.sql +102 -0
  191. package/package.json +85 -0
@@ -0,0 +1,694 @@
1
+ import path from "node:path";
2
+ import { createRuntimePaths, loadTwinnyConfig, resolveBundledBannerPath, resolveBundledLogoPath, resolveLarkAppSecret, SecurityCliSecretStore } from "../config/index.js";
3
+ import { ProfileCodexAppServerPool } from "../codex/index.js";
4
+ import { ConversationManager } from "../conversation/manager.js";
5
+ import { LarkEventConsumer, LarkFileDownloader, LarkMessageReader, LarkMessageSender, LarkBotDirectory, LarkChatDirectory, LarkUserDirectory, LarkOpenApiClient, resolveLarkEndpoints, resolveLarkEventDomain, TenantAccessTokenManager } from "../lark/index.js";
6
+ import { acquireTwinnyLock } from "../lock/index.js";
7
+ import { createLarkSdkLogger, createLogger, logger as defaultLogger } from "../observability/logs.js";
8
+ import { TwinnySystemNotifier } from "../observability/system-notifications.js";
9
+ import { getProfileCodexHome } from "../profiles/index.js";
10
+ import { createConversationRepository, openRuntimeDatabase } from "../store/index.js";
11
+ import { createTwinnyTelemetryClient, memoryUsageTelemetryProperties } from "../telemetry/index.js";
12
+ import { WorkspaceManager } from "../workspace/index.js";
13
+ import { MacIdleSleepPreventer } from "./caffeinate.js";
14
+ import { provisionLarkAssetImageKeys as provisionRuntimeLarkAssetImageKeys } from "./lark-assets.js";
15
+ export class TwinnyRuntime {
16
+ config;
17
+ options;
18
+ log;
19
+ larkSdkLogger;
20
+ paths;
21
+ secretStore = new SecurityCliSecretStore();
22
+ lock;
23
+ db;
24
+ codexPool;
25
+ idleSleepPreventer;
26
+ larkConsumer;
27
+ conversation;
28
+ systemNotifier;
29
+ codexRecoveryByProfile = new Map();
30
+ codexIntentionalStopByProfile = new Set();
31
+ telemetry;
32
+ heartbeatTimer;
33
+ runtimeStartedAt = 0;
34
+ stopped = false;
35
+ stopPromise;
36
+ resolveStopped;
37
+ constructor(config, options = {}) {
38
+ this.config = config;
39
+ this.options = options;
40
+ this.log = options.logger ?? defaultLogger;
41
+ this.paths = createRuntimePaths(config.home);
42
+ this.larkSdkLogger =
43
+ options.larkSdkLogger ??
44
+ createLarkSdkLogger(createLogger({ logFile: path.join(this.paths.logsDir, "lark-sdk.log") }));
45
+ this.telemetry =
46
+ options.telemetry ??
47
+ createTwinnyTelemetryClient(config, {
48
+ logger: this.log,
49
+ codexVersion: () => this.readRuntimeCodexVersion()
50
+ });
51
+ this.stopPromise = new Promise((resolve) => {
52
+ this.resolveStopped = resolve;
53
+ });
54
+ }
55
+ async start() {
56
+ const launchStartedAt = Date.now();
57
+ this.runtimeStartedAt = launchStartedAt;
58
+ let lockAcquired = false;
59
+ let dbOpened = false;
60
+ let recoveryAttempted = false;
61
+ let larkConsumerStarted = false;
62
+ try {
63
+ this.lock = await acquireTwinnyLock(this.paths, { stale: 30_000, update: 10_000 });
64
+ lockAcquired = true;
65
+ this.idleSleepPreventer = this.options.idleSleepPreventer ?? new MacIdleSleepPreventer({ logger: this.log });
66
+ this.idleSleepPreventer.start();
67
+ this.db = openRuntimeDatabase(this.paths);
68
+ dbOpened = true;
69
+ const appSecretAccount = this.config.homeIdentity.keychainAccounts.larkAppSecret;
70
+ const appSecret = await resolveLarkAppSecret(appSecretAccount, this.secretStore);
71
+ if (!appSecret) {
72
+ throw new Error(`Lark app secret is missing: keychain:${appSecretAccount}`);
73
+ }
74
+ this.codexPool = new ProfileCodexAppServerPool({
75
+ binary: this.config.codex.binary,
76
+ profiles: this.config.profiles,
77
+ requestTimeoutMs: this.options.requestTimeoutMs ?? 10 * 60 * 1000
78
+ });
79
+ for (const profile of Object.keys(this.config.profiles)) {
80
+ this.attachCodexAppServerListeners(profile, this.codexPool.get(profile));
81
+ }
82
+ await this.codexPool.startAll();
83
+ const tokenManager = new TenantAccessTokenManager({
84
+ appId: this.config.auth.larkAppId,
85
+ appSecret,
86
+ baseUrl: resolveLarkEndpoints(this.config.auth.larkBrand).openApi
87
+ });
88
+ const openApiClient = new LarkOpenApiClient({
89
+ tokenManager,
90
+ baseUrl: resolveLarkEndpoints(this.config.auth.larkBrand).openApi
91
+ });
92
+ const larkSender = new LarkMessageSender({
93
+ openApiClient,
94
+ logger: this.log,
95
+ redaction: this.config.lark.messageRedaction
96
+ });
97
+ const larkMessages = new LarkMessageReader({ openApiClient });
98
+ const larkUsers = new LarkUserDirectory({ openApiClient });
99
+ const larkChats = new LarkChatDirectory({ openApiClient });
100
+ const larkBot = new LarkBotDirectory({ openApiClient });
101
+ const botOpenId = await larkBot.getBotOpenId().catch((error) => {
102
+ this.log.warn({ error }, "failed to resolve lark bot open_id; group @mention matching will be unavailable");
103
+ return undefined;
104
+ });
105
+ const larkFiles = new LarkFileDownloader({ openApiClient });
106
+ const assetImageKeys = await this.provisionLarkAssetImageKeys(larkFiles);
107
+ const systemNotifier = new TwinnySystemNotifier({
108
+ ownerOpenId: this.config.owner.openId,
109
+ sender: larkSender,
110
+ logger: this.log
111
+ });
112
+ this.systemNotifier = systemNotifier;
113
+ const repository = createConversationRepository(this.db);
114
+ const workspaceManager = WorkspaceManager.fromRuntimePaths(this.paths);
115
+ const conversation = new ConversationManager({
116
+ config: this.config,
117
+ repository: adaptConversationRepository(repository),
118
+ workspaces: workspaceManager,
119
+ codex: adaptCodexPool(this.codexPool),
120
+ lark: adaptLarkSender(larkSender, this.config.lark.workingReaction, this.config.lark.completedReaction, this.config.lark.queuedReaction),
121
+ larkUsers,
122
+ larkChats,
123
+ larkFiles,
124
+ larkMessages,
125
+ botOpenId,
126
+ assetImageKeys,
127
+ profiles: { codexHomeFor: (profile) => getProfileCodexHome(this.config, profile) },
128
+ runtime: { reloadProfile: (profile) => this.reloadProfile(profile) },
129
+ telemetry: this.telemetry,
130
+ logger: this.log
131
+ });
132
+ this.conversation = conversation;
133
+ recoveryAttempted = true;
134
+ await conversation.recoverUnfinishedMessages();
135
+ this.larkConsumer = new LarkEventConsumer({
136
+ appId: this.config.auth.larkAppId,
137
+ appSecret,
138
+ tokenManager,
139
+ domain: resolveLarkEventDomain(this.config.auth.larkBrand),
140
+ botOpenId,
141
+ logger: this.log,
142
+ sdkLogger: this.larkSdkLogger,
143
+ maxMessageAgeMs: this.config.lark.maxMessageAgeSeconds * 1000,
144
+ onMessage: (message) => {
145
+ conversation.submitIncoming(message);
146
+ },
147
+ onMessageRecall: (recall) => {
148
+ conversation.submitMessageRecall(recall);
149
+ },
150
+ onBotMenu: (action) => {
151
+ conversation.submitBotMenuAction(action);
152
+ },
153
+ onCardAction: (action) => {
154
+ conversation.submitCardAction(action);
155
+ },
156
+ onConnectionError: (error) => {
157
+ this.telemetry.captureError(error, {
158
+ errorType: "lark_event",
159
+ errorSite: "lark.eventConsumer.connection",
160
+ operation: "connection_error",
161
+ fatal: false
162
+ });
163
+ },
164
+ onIgnored: (reason) => this.log.debug({ reason }, "lark event ignored")
165
+ });
166
+ await this.larkConsumer.start();
167
+ larkConsumerStarted = true;
168
+ await this.systemNotifier.notifyInitialized({ bannerImageKey: assetImageKeys.bannerImageKey });
169
+ this.telemetry.capture("twinny_launch", {
170
+ launch_duration_ms: Date.now() - launchStartedAt,
171
+ codex_profile_count: this.codexPool.listProfiles().length,
172
+ lark_consumer_started: larkConsumerStarted,
173
+ lark_ready: this.larkConsumer.isReady,
174
+ has_bot_open_id: botOpenId !== undefined,
175
+ db_opened: dbOpened,
176
+ lock_acquired: lockAcquired,
177
+ recovery_attempted: recoveryAttempted,
178
+ ...memoryUsageTelemetryProperties()
179
+ }, {
180
+ insertId: `twinny_launch:${this.telemetry.runtimeId}`,
181
+ codexVersion: this.readRuntimeCodexVersion()
182
+ });
183
+ this.startHeartbeat();
184
+ this.log.info({ home: this.config.home }, "twinny daemon started");
185
+ }
186
+ catch (error) {
187
+ this.telemetry.captureError(error, {
188
+ errorType: "runtime_start",
189
+ errorSite: "runtime.start",
190
+ operation: "start",
191
+ fatal: true,
192
+ properties: {
193
+ launch_duration_ms: Date.now() - launchStartedAt,
194
+ lark_consumer_started: larkConsumerStarted,
195
+ db_opened: dbOpened,
196
+ lock_acquired: lockAcquired,
197
+ recovery_attempted: recoveryAttempted
198
+ }
199
+ });
200
+ await this.cleanupAfterStartFailure(error);
201
+ throw error;
202
+ }
203
+ }
204
+ async cleanupAfterStartFailure(error) {
205
+ if (this.stopped) {
206
+ return;
207
+ }
208
+ this.stopped = true;
209
+ this.log.error({ error }, "twinny daemon failed to start; cleaning up");
210
+ try {
211
+ await this.shutdownConversation();
212
+ await this.stopLarkConsumer();
213
+ await this.stopCodexPool("SIGTERM");
214
+ this.closeDatabase();
215
+ }
216
+ finally {
217
+ await this.releaseLock();
218
+ await this.stopIdleSleepPreventer();
219
+ await this.shutdownTelemetry();
220
+ this.resolveStopped();
221
+ }
222
+ }
223
+ async stop(signal = "SIGTERM") {
224
+ if (this.stopped) {
225
+ return;
226
+ }
227
+ this.stopped = true;
228
+ this.log.info({ signal }, "stopping twinny daemon");
229
+ try {
230
+ this.stopHeartbeat();
231
+ await this.shutdownConversation();
232
+ await this.stopLarkConsumer();
233
+ await this.stopCodexPool(signal);
234
+ this.closeDatabase();
235
+ }
236
+ finally {
237
+ await this.releaseLock();
238
+ await this.stopIdleSleepPreventer(signal);
239
+ await this.shutdownTelemetry();
240
+ this.resolveStopped();
241
+ }
242
+ }
243
+ async wait() {
244
+ await this.stopPromise;
245
+ }
246
+ async handleCodexAppServerExit(profile) {
247
+ if (this.stopped) {
248
+ return;
249
+ }
250
+ const existing = this.codexRecoveryByProfile.get(profile);
251
+ if (existing) {
252
+ return existing;
253
+ }
254
+ const recovery = this.recoverCodexAppServer(profile).finally(() => {
255
+ if (this.codexRecoveryByProfile.get(profile) === recovery) {
256
+ this.codexRecoveryByProfile.delete(profile);
257
+ }
258
+ });
259
+ this.codexRecoveryByProfile.set(profile, recovery);
260
+ await recovery;
261
+ }
262
+ async recoverCodexAppServer(profile) {
263
+ const pool = this.codexPool;
264
+ if (!pool || this.stopped) {
265
+ return;
266
+ }
267
+ const suspended = (await this.conversation?.suspendActiveTurnsForCodexAppServerExit(profile)) ?? 0;
268
+ this.log.warn({ profile, suspended }, "recovering codex app-server after exit");
269
+ try {
270
+ await pool.restart(profile);
271
+ if (this.stopped) {
272
+ return;
273
+ }
274
+ const recovered = (await this.conversation?.recoverSuspendedActiveTurnsForCodexAppServerExit(profile)) ?? 0;
275
+ this.log.info({ profile, suspended, recovered }, "codex app-server recovered after exit");
276
+ }
277
+ catch (error) {
278
+ this.telemetry.captureError(error, {
279
+ errorType: "codex_app_server",
280
+ errorSite: "runtime.recoverCodexAppServer",
281
+ operation: "recover_codex_app_server",
282
+ fatal: false,
283
+ properties: { profile, suspended }
284
+ });
285
+ throw error;
286
+ }
287
+ }
288
+ attachCodexAppServerListeners(profile, server) {
289
+ server.on("stderr", (chunk) => {
290
+ this.log.debug({ profile, stream: "stderr", chunk }, "codex app-server stderr");
291
+ });
292
+ server.on("threadNameUpdated", (update) => {
293
+ this.conversation?.submitCodexThreadNameUpdated(update);
294
+ });
295
+ server.on("exit", (code, signal) => {
296
+ if (this.codexIntentionalStopByProfile.delete(profile)) {
297
+ this.log.info({ profile, code, signal }, "codex app-server stopped intentionally");
298
+ return;
299
+ }
300
+ this.log.error({ profile, code, signal }, "codex app-server exited");
301
+ this.telemetry.captureError(new Error("Codex app-server exited"), {
302
+ errorType: "codex_app_server",
303
+ errorSite: "runtime.codexAppServer.exit",
304
+ operation: "codex_app_server_exit",
305
+ fatal: false,
306
+ properties: { profile, code, signal }
307
+ });
308
+ void this.handleCodexAppServerExit(profile).catch((error) => {
309
+ this.log.error({ error, profile }, "failed to recover codex app-server after exit");
310
+ });
311
+ });
312
+ }
313
+ async reloadProfile(profile) {
314
+ const pool = this.codexPool;
315
+ if (!pool) {
316
+ throw new Error("Codex app-server pool is not started");
317
+ }
318
+ if (profile === "none") {
319
+ throw new Error("profile none is reserved");
320
+ }
321
+ const nextConfig = await loadTwinnyConfig({ home: this.config.home });
322
+ if (profile && !nextConfig.profiles[profile]) {
323
+ throw new Error(`Unknown profile: ${profile}`);
324
+ }
325
+ const currentProfiles = new Set(pool.listProfiles());
326
+ const nextProfiles = new Set(Object.keys(nextConfig.profiles));
327
+ const profilesToReload = profile
328
+ ? [profile]
329
+ : Array.from(new Set([...currentProfiles, ...nextProfiles]));
330
+ this.log.info({ profiles: profilesToReload }, "reloading twinny profiles");
331
+ for (const profileName of profilesToReload) {
332
+ const nextProfile = nextConfig.profiles[profileName];
333
+ if (!nextProfile) {
334
+ if (currentProfiles.has(profileName)) {
335
+ await this.stopCodexAppServerForReload(pool, profileName);
336
+ }
337
+ continue;
338
+ }
339
+ const suspended = currentProfiles.has(profileName)
340
+ ? (await this.conversation?.suspendActiveTurnsForCodexAppServerExit(profileName)) ?? 0
341
+ : 0;
342
+ if (currentProfiles.has(profileName)) {
343
+ await this.stopCodexAppServerForReload(pool, profileName);
344
+ }
345
+ const server = pool.replace(profileName, {
346
+ binary: nextConfig.codex.binary,
347
+ codexHome: nextProfile.codexHome
348
+ });
349
+ this.attachCodexAppServerListeners(profileName, server);
350
+ await server.start();
351
+ const recovered = currentProfiles.has(profileName)
352
+ ? (await this.conversation?.recoverSuspendedActiveTurnsForCodexAppServerExit(profileName)) ?? 0
353
+ : 0;
354
+ this.log.info({ profile: profileName, suspended, recovered }, "reloaded codex app-server profile");
355
+ }
356
+ replaceTwinnyConfigContents(this.config, nextConfig);
357
+ }
358
+ async stopCodexAppServerForReload(pool, profile) {
359
+ this.codexIntentionalStopByProfile.add(profile);
360
+ try {
361
+ await pool.remove(profile);
362
+ }
363
+ finally {
364
+ this.codexIntentionalStopByProfile.delete(profile);
365
+ }
366
+ }
367
+ async shutdownConversation() {
368
+ if (!this.conversation) {
369
+ return;
370
+ }
371
+ const conversation = this.conversation;
372
+ this.conversation = undefined;
373
+ try {
374
+ await conversation.shutdown();
375
+ }
376
+ catch (error) {
377
+ this.log.warn({ error }, "failed to shutdown conversation manager cleanly");
378
+ }
379
+ }
380
+ async stopLarkConsumer() {
381
+ if (!this.larkConsumer) {
382
+ return;
383
+ }
384
+ const consumer = this.larkConsumer;
385
+ this.larkConsumer = undefined;
386
+ try {
387
+ await consumer.stop({ force: true });
388
+ }
389
+ catch (error) {
390
+ this.log.warn({ error }, "failed to stop lark event consumer cleanly");
391
+ }
392
+ }
393
+ async stopCodexPool(signal) {
394
+ if (!this.codexPool) {
395
+ return;
396
+ }
397
+ const pool = this.codexPool;
398
+ this.codexPool = undefined;
399
+ try {
400
+ await pool.stopAll(signal);
401
+ }
402
+ catch (error) {
403
+ this.log.warn({ error }, "failed to stop codex app-server pool cleanly");
404
+ }
405
+ }
406
+ closeDatabase() {
407
+ if (!this.db) {
408
+ return;
409
+ }
410
+ const db = this.db;
411
+ this.db = undefined;
412
+ try {
413
+ db.close();
414
+ }
415
+ catch (error) {
416
+ this.log.warn({ error }, "failed to close sqlite database cleanly");
417
+ }
418
+ }
419
+ async releaseLock() {
420
+ if (!this.lock) {
421
+ return;
422
+ }
423
+ const lock = this.lock;
424
+ this.lock = undefined;
425
+ try {
426
+ await lock.release();
427
+ }
428
+ catch (error) {
429
+ this.log.warn({ error }, "failed to release runtime lock cleanly");
430
+ }
431
+ }
432
+ async stopIdleSleepPreventer(signal = "SIGTERM") {
433
+ if (!this.idleSleepPreventer) {
434
+ return;
435
+ }
436
+ const preventer = this.idleSleepPreventer;
437
+ this.idleSleepPreventer = undefined;
438
+ try {
439
+ await preventer.stop(signal);
440
+ }
441
+ catch (error) {
442
+ this.log.warn({ error }, "failed to stop caffeinate idle sleep assertion cleanly");
443
+ }
444
+ }
445
+ async provisionLarkAssetImageKeys(larkFiles) {
446
+ return provisionRuntimeLarkAssetImageKeys({
447
+ cacheFile: this.paths.larkAssetsFile,
448
+ logoFilePath: this.options.logoFilePath ?? resolveBundledLogoPath(),
449
+ bannerFilePath: this.options.bannerFilePath ?? resolveBundledBannerPath(),
450
+ uploader: larkFiles,
451
+ logger: this.log
452
+ });
453
+ }
454
+ startHeartbeat() {
455
+ if (this.options.disableHeartbeat) {
456
+ return;
457
+ }
458
+ const intervalMs = this.options.heartbeatIntervalMs ?? 60 * 60 * 1000;
459
+ this.heartbeatTimer = setInterval(() => this.captureHeartbeat(intervalMs), intervalMs);
460
+ this.heartbeatTimer.unref?.();
461
+ }
462
+ stopHeartbeat() {
463
+ if (!this.heartbeatTimer) {
464
+ return;
465
+ }
466
+ clearInterval(this.heartbeatTimer);
467
+ this.heartbeatTimer = undefined;
468
+ }
469
+ captureHeartbeat(intervalMs) {
470
+ const stats = this.conversation?.getRuntimeStats() ?? {
471
+ activeTurnCount: 0,
472
+ sideTurnCount: 0,
473
+ queuedMessageCount: 0,
474
+ suspendedTurnCount: 0
475
+ };
476
+ this.telemetry.capture("twinny_heartbeat", {
477
+ uptime_ms: Date.now() - this.runtimeStartedAt,
478
+ lark_consumer_running: this.larkConsumer?.isRunning ?? false,
479
+ lark_ready: this.larkConsumer?.isReady ?? false,
480
+ lark_connection_status: safeConnectionStatus(this.larkConsumer?.getConnectionStatus()),
481
+ codex_profile_count: this.codexPool?.listProfiles().length ?? 0,
482
+ active_turn_count: stats.activeTurnCount,
483
+ side_turn_count: stats.sideTurnCount,
484
+ queued_message_count: stats.queuedMessageCount,
485
+ suspended_turn_count: stats.suspendedTurnCount,
486
+ ...memoryUsageTelemetryProperties()
487
+ }, {
488
+ insertId: `twinny_heartbeat:${this.telemetry.runtimeId}:${Math.floor(Date.now() / intervalMs)}`,
489
+ codexVersion: this.readRuntimeCodexVersion()
490
+ });
491
+ }
492
+ readRuntimeCodexVersion() {
493
+ const profile = this.codexPool?.listProfiles()[0];
494
+ return profile ? this.codexPool?.get(profile).readCodexVersion() ?? null : null;
495
+ }
496
+ async shutdownTelemetry() {
497
+ try {
498
+ await this.telemetry.shutdown?.();
499
+ }
500
+ catch (error) {
501
+ this.log.warn({ error }, "failed to shutdown telemetry");
502
+ }
503
+ }
504
+ }
505
+ export async function createRuntime(config, options = {}) {
506
+ return new TwinnyRuntime(config, options);
507
+ }
508
+ function replaceTwinnyConfigContents(target, source) {
509
+ Object.assign(target, source);
510
+ }
511
+ function safeConnectionStatus(status) {
512
+ if (status === undefined || status === null) {
513
+ return null;
514
+ }
515
+ if (typeof status === "string" || typeof status === "number" || typeof status === "boolean") {
516
+ return String(status);
517
+ }
518
+ try {
519
+ return JSON.stringify(status).slice(0, 256);
520
+ }
521
+ catch {
522
+ return String(status).slice(0, 256);
523
+ }
524
+ }
525
+ export function adaptConversationRepository(repository) {
526
+ return {
527
+ findByConversationKey: (conversationKey) => repository.getByConversationKey(conversationKey) ?? null,
528
+ create: repository.create.bind(repository),
529
+ updateThreadBinding: repository.updateThreadBinding.bind(repository),
530
+ updateConversationSettings: repository.updateConversationSettings.bind(repository),
531
+ markThreadHasRollout: repository.markThreadHasRollout.bind(repository),
532
+ getCodexThreadById: repository.getCodexThreadById.bind(repository),
533
+ getCodexThreadByConversationAndLarkThread: repository.getCodexThreadByConversationAndLarkThread.bind(repository),
534
+ getLarkMessageById: repository.getLarkMessageById.bind(repository),
535
+ getLarkMessageByEventId: repository.getLarkMessageByEventId.bind(repository),
536
+ getLarkMessageUsageTargetForTurn: repository.getLarkMessageUsageTargetForTurn.bind(repository),
537
+ getLatestSteeredLarkMessageForTurn: repository.getLatestSteeredLarkMessageForTurn.bind(repository),
538
+ listUnfinishedLarkMessages: repository.listUnfinishedLarkMessages.bind(repository),
539
+ upsertCodexThread: repository.upsertCodexThread.bind(repository),
540
+ replaceCodexThreadForLarkThread: repository.replaceCodexThreadForLarkThread.bind(repository),
541
+ updateCodexThreadTokenUsage: repository.updateCodexThreadTokenUsage.bind(repository),
542
+ updateCodexThreadGoalStatus: repository.updateCodexThreadGoalStatus.bind(repository),
543
+ clearCodexThreadGoalStatus: repository.clearCodexThreadGoalStatus.bind(repository),
544
+ updateCodexThreadCard: repository.updateCodexThreadCard.bind(repository),
545
+ updateCodexThreadModelSettings: repository.updateCodexThreadModelSettings.bind(repository),
546
+ updateCodexThreadName: repository.updateCodexThreadName.bind(repository),
547
+ updateCodexThreadMode: repository.updateCodexThreadMode.bind(repository),
548
+ updateCodexThreadStatus: repository.updateCodexThreadStatus.bind(repository),
549
+ getCodexThreadWorkStats: repository.getCodexThreadWorkStats.bind(repository),
550
+ getCodexThreadStatusStats: repository.getCodexThreadStatusStats.bind(repository),
551
+ getConversationStatusStats: repository.getConversationStatusStats.bind(repository),
552
+ insertLarkMessage: repository.insertLarkMessage.bind(repository),
553
+ markLarkMessageQueued: repository.markLarkMessageQueued.bind(repository),
554
+ markLarkMessageRecalled: repository.markLarkMessageRecalled.bind(repository),
555
+ updateQueuedLarkMessage: repository.updateQueuedLarkMessage.bind(repository),
556
+ updateLarkMessageSideMetadata: repository.updateLarkMessageSideMetadata.bind(repository),
557
+ updateLarkMessageTokenUsage: repository.updateLarkMessageTokenUsage.bind(repository),
558
+ markLarkMessagesProcessing: repository.markLarkMessagesProcessing.bind(repository),
559
+ markLarkMessagesSteered: repository.markLarkMessagesSteered.bind(repository),
560
+ markLarkMessagesCompleted: repository.markLarkMessagesCompleted.bind(repository),
561
+ markLarkMessagesFailed: repository.markLarkMessagesFailed.bind(repository),
562
+ markLarkMessagesInterrupted: repository.markLarkMessagesInterrupted.bind(repository),
563
+ markLarkMessagesCleared: repository.markLarkMessagesCleared.bind(repository)
564
+ };
565
+ }
566
+ function adaptCodexPool(pool) {
567
+ return {
568
+ startThread: async ({ profile, cwd, developerInstructions }) => {
569
+ const response = await pool.get(profile).startThread(cwd, { developerInstructions });
570
+ return { threadId: response.thread.id };
571
+ },
572
+ resumeThread: async ({ profile, threadId, cwd }) => {
573
+ const response = await pool.get(profile).resumeThread(threadId, cwd);
574
+ return { threadId: response.thread.id };
575
+ },
576
+ forkThread: async ({ profile, threadId, cwd, ephemeral, developerInstructions, model, effort }) => {
577
+ const response = await pool.get(profile).forkThread(threadId, cwd, {
578
+ ephemeral,
579
+ developerInstructions,
580
+ model,
581
+ effort
582
+ });
583
+ return { threadId: response.thread.id };
584
+ },
585
+ injectThreadItems: async ({ profile, threadId, items }) => {
586
+ await pool.get(profile).injectThreadItems(threadId, items);
587
+ },
588
+ unsubscribeThread: async ({ profile, threadId }) => {
589
+ await pool.get(profile).unsubscribeThread(threadId);
590
+ },
591
+ startTurn: async ({ profile, threadId, input, currentThreadName, cwd, mode, model, effort, onTurnStarted, onAgentMessage, onImageGeneration, onTokenUsage, onPlanUpdated, onRequestUserInput, onSetThreadName }) => pool.get(profile).startTurn({
592
+ threadId,
593
+ ...(typeof input === "string" ? { text: input } : { input }),
594
+ currentThreadName,
595
+ cwd,
596
+ mode,
597
+ model,
598
+ effort,
599
+ onTurnStarted,
600
+ onAgentMessage,
601
+ onImageGeneration,
602
+ onTokenUsage,
603
+ onPlanUpdated,
604
+ onRequestUserInput,
605
+ onSetThreadName
606
+ }),
607
+ compactThread: async ({ profile, threadId, cwd, onTurnStarted, onTokenUsage }) => pool.get(profile).compactThread({
608
+ threadId,
609
+ cwd,
610
+ onTurnStarted,
611
+ onTokenUsage
612
+ }),
613
+ setThreadGoal: async ({ profile, threadId, objective }) => pool.get(profile).setThreadGoal(threadId, objective),
614
+ getThreadGoal: async ({ profile, threadId }) => pool.get(profile).getThreadGoal(threadId),
615
+ clearThreadGoal: async ({ profile, threadId }) => {
616
+ await pool.get(profile).clearThreadGoal(threadId);
617
+ },
618
+ setThreadName: async ({ profile, threadId, name }) => {
619
+ await pool.get(profile).setThreadName(threadId, name);
620
+ },
621
+ runGoal: async ({ profile, ...options }) => pool.get(profile).runGoal(options),
622
+ resumeGoal: async ({ profile, ...options }) => pool.get(profile).resumeGoal(options),
623
+ steerTurn: async ({ profile, threadId, turnId, input }) => {
624
+ await pool.get(profile).steerTurn({
625
+ threadId,
626
+ turnId,
627
+ ...(typeof input === "string" ? { text: input } : { input })
628
+ });
629
+ },
630
+ interruptTurn: async ({ profile, threadId, turnId }) => {
631
+ await pool.get(profile).interruptTurn({ threadId, turnId });
632
+ },
633
+ readCodexVersion: ({ profile }) => {
634
+ return pool.get(profile).readCodexVersion();
635
+ },
636
+ readAccountRateLimits: async ({ profile }) => {
637
+ return pool.get(profile).readAccountRateLimits();
638
+ }
639
+ };
640
+ }
641
+ function adaptLarkSender(sender, workingReaction, completedReaction, queuedReaction) {
642
+ return {
643
+ addTypingReaction: (messageId) => sender.createReaction(messageId, workingReaction),
644
+ addCompletedReaction: (messageId) => sender.createReaction(messageId, completedReaction),
645
+ addQueuedReaction: (messageId) => sender.createReaction(messageId, queuedReaction),
646
+ removeReaction: (handle) => sender.deleteReaction(handle),
647
+ replyText: async (messageId, text, options) => {
648
+ return sender.replyText(messageId, text, options);
649
+ },
650
+ replyMarkdown: async (messageId, markdown, options) => {
651
+ return sender.replyMarkdown(messageId, markdown, options);
652
+ },
653
+ replyPost: async (messageId, content, options) => {
654
+ return sender.replyPost(messageId, content, options);
655
+ },
656
+ replyFile: async (messageId, fileKey) => {
657
+ return sender.replyFile(messageId, fileKey);
658
+ },
659
+ replyImage: async (messageId, imageKey) => {
660
+ return sender.replyImage(messageId, imageKey);
661
+ },
662
+ sendTextToOpenId: async (openId, text) => {
663
+ await sender.sendTextToOpenId(openId, text);
664
+ },
665
+ sendCardToOpenId: async (openId, card, options) => {
666
+ return sender.sendInteractiveCardToOpenId(openId, card, options);
667
+ },
668
+ sendCardToChatId: async (chatId, card, options) => {
669
+ return sender.sendInteractiveCardToChatId(chatId, card, options);
670
+ },
671
+ sendEphemeralCardToChatId: async (chatId, openId, card) => {
672
+ return sender.sendEphemeralInteractiveCardToChatId(chatId, openId, card);
673
+ },
674
+ forwardThreadToThread: async (threadId, receiveThreadId, options) => {
675
+ return sender.forwardThreadToThread(threadId, receiveThreadId, options);
676
+ },
677
+ replyCard: async (messageId, card, options) => {
678
+ return sender.replyInteractiveCard(messageId, card, options);
679
+ },
680
+ patchCard: async (messageId, card) => {
681
+ return sender.patchInteractiveCard(messageId, card);
682
+ },
683
+ recallMessage: async (messageId) => {
684
+ await sender.deleteMessage(messageId);
685
+ },
686
+ deleteEphemeralMessage: async (messageId) => {
687
+ await sender.deleteEphemeralMessage(messageId);
688
+ },
689
+ getMessageReadOpenIds: async (messageId) => {
690
+ return sender.listMessageReadOpenIds(messageId);
691
+ }
692
+ };
693
+ }
694
+ //# sourceMappingURL=wiring.js.map