@covro/covro 0.0.0-stage → 0.1.0-dev.1

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -2
  3. package/README.zh-CN.md +174 -0
  4. package/bin/covro.mjs +8 -0
  5. package/dist/cli/control.js +66 -0
  6. package/dist/cli/control.js.map +7 -0
  7. package/dist/cli/daemon.js +118 -0
  8. package/dist/cli/daemon.js.map +7 -0
  9. package/dist/cli/main.js +152 -0
  10. package/dist/cli/main.js.map +7 -0
  11. package/dist/cli/paths.js +60 -0
  12. package/dist/cli/paths.js.map +7 -0
  13. package/dist/cli/service.js +87 -0
  14. package/dist/cli/service.js.map +7 -0
  15. package/dist/codex/adapter.js +1195 -0
  16. package/dist/codex/adapter.js.map +7 -0
  17. package/dist/codex/auto-review.js +102 -0
  18. package/dist/codex/auto-review.js.map +7 -0
  19. package/dist/codex/compatibility.js +7 -0
  20. package/dist/codex/compatibility.js.map +7 -0
  21. package/dist/codex/mapping.js +210 -0
  22. package/dist/codex/mapping.js.map +7 -0
  23. package/dist/codex/permissions.js +51 -0
  24. package/dist/codex/permissions.js.map +7 -0
  25. package/dist/codex/thread-title-policy.js +52 -0
  26. package/dist/codex/thread-title-policy.js.map +7 -0
  27. package/dist/codex/thread-titles.js +259 -0
  28. package/dist/codex/thread-titles.js.map +7 -0
  29. package/dist/codex/transport.js +236 -0
  30. package/dist/codex/transport.js.map +7 -0
  31. package/dist/codex/turn-token-usage.js +46 -0
  32. package/dist/codex/turn-token-usage.js.map +7 -0
  33. package/dist/codex/types.js +1 -0
  34. package/dist/codex/types.js.map +7 -0
  35. package/dist/drizzle/0000_past_wildside.sql +45 -0
  36. package/dist/drizzle/0001_shocking_ender_wiggin.sql +2 -0
  37. package/dist/drizzle/0002_allow_parallel_threads.sql +1 -0
  38. package/dist/drizzle/0003_project_registrations.sql +11 -0
  39. package/dist/drizzle/0004_turn_usage.sql +6 -0
  40. package/dist/drizzle/meta/0000_snapshot.json +309 -0
  41. package/dist/drizzle/meta/0001_snapshot.json +323 -0
  42. package/dist/drizzle/meta/0002_snapshot.json +315 -0
  43. package/dist/drizzle/meta/0003_snapshot.json +381 -0
  44. package/dist/drizzle/meta/0004_snapshot.json +420 -0
  45. package/dist/drizzle/meta/_journal.json +41 -0
  46. package/dist/server/auth.js +47 -0
  47. package/dist/server/auth.js.map +7 -0
  48. package/dist/server/compaction.js +40 -0
  49. package/dist/server/compaction.js.map +7 -0
  50. package/dist/server/config-store.js +147 -0
  51. package/dist/server/config-store.js.map +7 -0
  52. package/dist/server/config.js +124 -0
  53. package/dist/server/config.js.map +7 -0
  54. package/dist/server/db/index.js +21 -0
  55. package/dist/server/db/index.js.map +7 -0
  56. package/dist/server/db/migrate.js +26 -0
  57. package/dist/server/db/migrate.js.map +7 -0
  58. package/dist/server/db/operations.js +201 -0
  59. package/dist/server/db/operations.js.map +7 -0
  60. package/dist/server/db/schema.js +99 -0
  61. package/dist/server/db/schema.js.map +7 -0
  62. package/dist/server/db/turn-usage.js +31 -0
  63. package/dist/server/db/turn-usage.js.map +7 -0
  64. package/dist/server/events.js +123 -0
  65. package/dist/server/events.js.map +7 -0
  66. package/dist/server/http.js +305 -0
  67. package/dist/server/http.js.map +7 -0
  68. package/dist/server/images.js +49 -0
  69. package/dist/server/images.js.map +7 -0
  70. package/dist/server/instance-lock.js +25 -0
  71. package/dist/server/instance-lock.js.map +7 -0
  72. package/dist/server/local-files.js +100 -0
  73. package/dist/server/local-files.js.map +7 -0
  74. package/dist/server/main.js +125 -0
  75. package/dist/server/main.js.map +7 -0
  76. package/dist/server/markdown.js +28 -0
  77. package/dist/server/markdown.js.map +7 -0
  78. package/dist/server/projection.js +348 -0
  79. package/dist/server/projection.js.map +7 -0
  80. package/dist/server/projects.js +230 -0
  81. package/dist/server/projects.js.map +7 -0
  82. package/dist/server/service.js +1221 -0
  83. package/dist/server/service.js.map +7 -0
  84. package/dist/server/snapshot.js +98 -0
  85. package/dist/server/snapshot.js.map +7 -0
  86. package/dist/server/thread-export.js +97 -0
  87. package/dist/server/thread-export.js.map +7 -0
  88. package/dist/server/websocket.js +126 -0
  89. package/dist/server/websocket.js.map +7 -0
  90. package/dist/shared/async-questions.js +39 -0
  91. package/dist/shared/async-questions.js.map +7 -0
  92. package/dist/shared/codex-compatibility.js +56 -0
  93. package/dist/shared/codex-compatibility.js.map +7 -0
  94. package/dist/shared/elicitation.js +314 -0
  95. package/dist/shared/elicitation.js.map +7 -0
  96. package/dist/shared/errors.js +24 -0
  97. package/dist/shared/errors.js.map +7 -0
  98. package/dist/shared/gateway-config.js +1 -0
  99. package/dist/shared/gateway-config.js.map +7 -0
  100. package/dist/shared/images.js +21 -0
  101. package/dist/shared/images.js.map +7 -0
  102. package/dist/shared/package.js +5 -0
  103. package/dist/shared/package.js.map +7 -0
  104. package/dist/shared/permissions.js +63 -0
  105. package/dist/shared/permissions.js.map +7 -0
  106. package/dist/shared/thread-name.js +14 -0
  107. package/dist/shared/thread-name.js.map +7 -0
  108. package/dist/shared/thread-summary.js +24 -0
  109. package/dist/shared/thread-summary.js.map +7 -0
  110. package/dist/shared/types.js +1 -0
  111. package/dist/shared/types.js.map +7 -0
  112. package/dist/shared/updates.js +7 -0
  113. package/dist/shared/updates.js.map +7 -0
  114. package/dist/shared/validation.js +116 -0
  115. package/dist/shared/validation.js.map +7 -0
  116. package/dist/shared/versions.js +36 -0
  117. package/dist/shared/versions.js.map +7 -0
  118. package/dist/update-worker.cjs +21968 -0
  119. package/dist/updates/files.js +54 -0
  120. package/dist/updates/files.js.map +7 -0
  121. package/dist/updates/manager.js +315 -0
  122. package/dist/updates/manager.js.map +7 -0
  123. package/dist/updates/registry.js +71 -0
  124. package/dist/updates/registry.js.map +7 -0
  125. package/dist/updates/transaction.js +96 -0
  126. package/dist/updates/transaction.js.map +7 -0
  127. package/dist/updates/worker.js +345 -0
  128. package/dist/updates/worker.js.map +7 -0
  129. package/dist/web/assets/index-BW9afH_Y.js +83 -0
  130. package/dist/web/assets/index-L3S1ZiO0.css +1 -0
  131. package/dist/web/covro.svg +6 -0
  132. package/dist/web/index.html +15 -0
  133. package/package.json +88 -4
@@ -0,0 +1,147 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { access, lstat, open, readFile, rename, rm } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import lockfile from "proper-lockfile";
6
+ import { z } from "zod";
7
+ import { AppError } from "../shared/errors.js";
8
+ import { configSource, parseConfig, parseFileConfig } from "./config.js";
9
+ const saveSchema = z.object({ revision: z.string().regex(/^[a-f0-9]{64}$/), config: z.unknown() }).strict();
10
+ const importSchema = z.object({ config: z.unknown() }).strict();
11
+ const revision = (text) => createHash("sha256").update(text).digest("hex");
12
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
13
+ const MAX_CONFIG_BYTES = 256 * 1024;
14
+ class ConfigStore {
15
+ source;
16
+ running;
17
+ constructor(config) {
18
+ this.source = configSource(config);
19
+ const { demo: _demo, ...running } = config;
20
+ this.running = structuredClone(running);
21
+ }
22
+ async read() {
23
+ if (!this.source)
24
+ throw new AppError("CONFIG_UNAVAILABLE", "\u6B64\u5B9E\u4F8B\u672A\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\uFF0C\u65E0\u6CD5\u7BA1\u7406\u670D\u52A1\u914D\u7F6E\u3002", 503);
25
+ try {
26
+ const stat = await lstat(this.source.path);
27
+ if (stat.size > MAX_CONFIG_BYTES)
28
+ throw new AppError("INVALID_CONFIG", "\u914D\u7F6E\u6587\u4EF6\u4E0D\u80FD\u8D85\u8FC7 256 KiB\u3002");
29
+ const text = await readFile(this.source.path, "utf8");
30
+ if (Buffer.byteLength(text) > MAX_CONFIG_BYTES)
31
+ throw new AppError("INVALID_CONFIG", "\u914D\u7F6E\u6587\u4EF6\u4E0D\u80FD\u8D85\u8FC7 256 KiB\u3002");
32
+ let value;
33
+ try {
34
+ value = JSON.parse(text);
35
+ } catch {
36
+ throw new AppError("INVALID_CONFIG", "\u914D\u7F6E\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 JSON\uFF0C\u8BF7\u5148\u4FEE\u590D\u6587\u4EF6\u3002");
37
+ }
38
+ return { saved: parseFileConfig(value), revision: revision(text), stat };
39
+ } catch (error) {
40
+ if (error instanceof AppError) throw error;
41
+ throw new AppError(
42
+ "CONFIG_READ_FAILED",
43
+ "\u65E0\u6CD5\u8BFB\u53D6\u542F\u52A8\u65F6\u6307\u5B9A\u7684\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u548C\u6743\u9650\u3002",
44
+ 503
45
+ );
46
+ }
47
+ }
48
+ async snapshot() {
49
+ const file = await this.read();
50
+ let readOnlyReason;
51
+ if (!file.stat.isFile())
52
+ readOnlyReason = "\u8F6F\u94FE\u63A5\u6216\u7279\u6B8A\u914D\u7F6E\u6587\u4EF6\u53EA\u652F\u6301\u67E5\u770B\u548C\u5BFC\u51FA\uFF0C\u8BF7\u76F4\u63A5\u7F16\u8F91\u6E90\u6587\u4EF6\u3002";
53
+ else {
54
+ try {
55
+ await access(this.source.path, constants.W_OK);
56
+ await access(dirname(this.source.path), constants.W_OK);
57
+ } catch {
58
+ readOnlyReason = "\u914D\u7F6E\u6587\u4EF6\u6216\u6240\u5728\u76EE\u5F55\u4E0D\u53EF\u5199\uFF0C\u8BF7\u8C03\u6574\u670D\u52A1\u8FDB\u7A0B\u7684\u6587\u4EF6\u6743\u9650\u3002";
59
+ }
60
+ }
61
+ const { demo: _demo, ...effective } = parseConfig(
62
+ file.saved,
63
+ this.source.path,
64
+ this.source.development
65
+ );
66
+ return {
67
+ path: this.source.path,
68
+ revision: file.revision,
69
+ saved: file.saved,
70
+ running: this.running,
71
+ restartRequired: !same(effective, this.running),
72
+ writable: !readOnlyReason,
73
+ ...readOnlyReason ? { readOnlyReason } : {}
74
+ };
75
+ }
76
+ async previewImport(input) {
77
+ const imported = parseFileConfig(importSchema.parse(input).config);
78
+ const { saved } = await this.read();
79
+ const preserved = ["dataDir", "projects"].filter(
80
+ (key) => !same(imported[key], saved[key])
81
+ );
82
+ return { config: { ...imported, dataDir: saved.dataDir, projects: saved.projects }, preserved };
83
+ }
84
+ async export() {
85
+ return (await this.read()).saved;
86
+ }
87
+ async save(input) {
88
+ const parsed = saveSchema.parse(input);
89
+ const config = parseFileConfig(parsed.config);
90
+ const initial = await this.snapshot();
91
+ if (!initial.writable) throw new AppError("CONFIG_READ_ONLY", initial.readOnlyReason, 409);
92
+ let release;
93
+ const temporary = join(dirname(this.source.path), `.codex-config-${randomUUID()}.tmp`);
94
+ try {
95
+ try {
96
+ release = await lockfile.lock(this.source.path, { realpath: false, retries: 0 });
97
+ } catch {
98
+ throw new AppError("CONFIG_BUSY", "\u914D\u7F6E\u6B63\u5728\u88AB\u5176\u4ED6\u64CD\u4F5C\u4FDD\u5B58\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", 409);
99
+ }
100
+ const current = await this.snapshot();
101
+ if (!current.writable) throw new AppError("CONFIG_READ_ONLY", current.readOnlyReason, 409);
102
+ if (current.revision !== parsed.revision)
103
+ throw new AppError(
104
+ "CONFIG_CONFLICT",
105
+ "\u914D\u7F6E\u6587\u4EF6\u5DF2\u88AB\u5176\u4ED6\u9875\u9762\u6216\u7A0B\u5E8F\u4FEE\u6539\u3002\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u518D\u4FDD\u5B58\u3002",
106
+ 409
107
+ );
108
+ if (config.dataDir !== current.saved.dataDir || !same(config.projects, current.saved.projects))
109
+ throw new AppError(
110
+ "CONFIG_PROTECTED",
111
+ "\u6570\u636E\u76EE\u5F55\u4E0D\u53EF\u5728\u6B64\u4FEE\u6539\uFF1B\u9879\u76EE\u8BF7\u4F7F\u7528\u9879\u76EE\u7BA1\u7406\u529F\u80FD\u3002",
112
+ 400
113
+ );
114
+ const contents = `${JSON.stringify(config, null, 2)}
115
+ `;
116
+ if (Buffer.byteLength(contents) > MAX_CONFIG_BYTES)
117
+ throw new AppError("INVALID_CONFIG", "\u914D\u7F6E\u6587\u4EF6\u4E0D\u80FD\u8D85\u8FC7 256 KiB\u3002");
118
+ const file = await open(temporary, "wx", 384);
119
+ try {
120
+ await file.writeFile(contents, "utf8");
121
+ await file.sync();
122
+ } finally {
123
+ await file.close();
124
+ }
125
+ const latest = await this.snapshot();
126
+ if (latest.revision !== current.revision || !latest.writable)
127
+ throw new AppError("CONFIG_CONFLICT", "\u914D\u7F6E\u6587\u4EF6\u5DF2\u53D1\u751F\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u518D\u4FDD\u5B58\u3002", 409);
128
+ await rename(temporary, this.source.path);
129
+ return await this.snapshot();
130
+ } catch (error) {
131
+ if (error instanceof AppError) throw error;
132
+ throw new AppError(
133
+ "CONFIG_WRITE_FAILED",
134
+ "\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u6743\u9650\u548C\u78C1\u76D8\u7A7A\u95F4\u540E\u91CD\u8BD5\u3002",
135
+ 500
136
+ );
137
+ } finally {
138
+ await rm(temporary, { force: true });
139
+ await release?.();
140
+ }
141
+ }
142
+ }
143
+ export {
144
+ ConfigStore,
145
+ MAX_CONFIG_BYTES
146
+ };
147
+ //# sourceMappingURL=config-store.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/config-store.ts"],
4
+ "sourcesContent": ["import { createHash, randomUUID } from 'node:crypto';\nimport { constants } from 'node:fs';\nimport { access, lstat, open, readFile, rename, rm } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport lockfile from 'proper-lockfile';\nimport { z } from 'zod';\nimport { AppError } from '../shared/errors.js';\nimport type { GatewayConfig, GatewayConfigSnapshot } from '../shared/gateway-config.js';\nimport { configSource, parseConfig, parseFileConfig, type AppConfig } from './config.js';\n\nconst saveSchema = z\n .object({ revision: z.string().regex(/^[a-f0-9]{64}$/), config: z.unknown() })\n .strict();\nconst importSchema = z.object({ config: z.unknown() }).strict();\nconst revision = (text: string) => createHash('sha256').update(text).digest('hex');\nconst same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);\nexport const MAX_CONFIG_BYTES = 256 * 1024;\n\n/** The running configuration is immutable; this store only changes the next startup's file. */\nexport class ConfigStore {\n private readonly source;\n private readonly running: GatewayConfig;\n constructor(config: AppConfig) {\n this.source = configSource(config);\n const { demo: _demo, ...running } = config;\n this.running = structuredClone(running);\n }\n\n private async read() {\n if (!this.source)\n throw new AppError('CONFIG_UNAVAILABLE', '\u6B64\u5B9E\u4F8B\u672A\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\uFF0C\u65E0\u6CD5\u7BA1\u7406\u670D\u52A1\u914D\u7F6E\u3002', 503);\n try {\n const stat = await lstat(this.source.path);\n if (stat.size > MAX_CONFIG_BYTES)\n throw new AppError('INVALID_CONFIG', '\u914D\u7F6E\u6587\u4EF6\u4E0D\u80FD\u8D85\u8FC7 256 KiB\u3002');\n const text = await readFile(this.source.path, 'utf8');\n if (Buffer.byteLength(text) > MAX_CONFIG_BYTES)\n throw new AppError('INVALID_CONFIG', '\u914D\u7F6E\u6587\u4EF6\u4E0D\u80FD\u8D85\u8FC7 256 KiB\u3002');\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n throw new AppError('INVALID_CONFIG', '\u914D\u7F6E\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 JSON\uFF0C\u8BF7\u5148\u4FEE\u590D\u6587\u4EF6\u3002');\n }\n return { saved: parseFileConfig(value), revision: revision(text), stat };\n } catch (error) {\n if (error instanceof AppError) throw error;\n throw new AppError(\n 'CONFIG_READ_FAILED',\n '\u65E0\u6CD5\u8BFB\u53D6\u542F\u52A8\u65F6\u6307\u5B9A\u7684\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u548C\u6743\u9650\u3002',\n 503,\n );\n }\n }\n\n async snapshot(): Promise<GatewayConfigSnapshot> {\n const file = await this.read();\n let readOnlyReason: string | undefined;\n if (!file.stat.isFile())\n readOnlyReason = '\u8F6F\u94FE\u63A5\u6216\u7279\u6B8A\u914D\u7F6E\u6587\u4EF6\u53EA\u652F\u6301\u67E5\u770B\u548C\u5BFC\u51FA\uFF0C\u8BF7\u76F4\u63A5\u7F16\u8F91\u6E90\u6587\u4EF6\u3002';\n else {\n try {\n await access(this.source!.path, constants.W_OK);\n await access(dirname(this.source!.path), constants.W_OK);\n } catch {\n readOnlyReason = '\u914D\u7F6E\u6587\u4EF6\u6216\u6240\u5728\u76EE\u5F55\u4E0D\u53EF\u5199\uFF0C\u8BF7\u8C03\u6574\u670D\u52A1\u8FDB\u7A0B\u7684\u6587\u4EF6\u6743\u9650\u3002';\n }\n }\n const { demo: _demo, ...effective } = parseConfig(\n file.saved,\n this.source!.path,\n this.source!.development,\n );\n return {\n path: this.source!.path,\n revision: file.revision,\n saved: file.saved,\n running: this.running,\n restartRequired: !same(effective, this.running),\n writable: !readOnlyReason,\n ...(readOnlyReason ? { readOnlyReason } : {}),\n };\n }\n\n async previewImport(input: unknown) {\n const imported = parseFileConfig(importSchema.parse(input).config);\n const { saved } = await this.read();\n const preserved = (['dataDir', 'projects'] as const).filter(\n (key) => !same(imported[key], saved[key]),\n );\n return { config: { ...imported, dataDir: saved.dataDir, projects: saved.projects }, preserved };\n }\n\n async export() {\n return (await this.read()).saved;\n }\n\n async save(input: unknown): Promise<GatewayConfigSnapshot> {\n const parsed = saveSchema.parse(input);\n const config = parseFileConfig(parsed.config);\n const initial = await this.snapshot();\n if (!initial.writable) throw new AppError('CONFIG_READ_ONLY', initial.readOnlyReason!, 409);\n let release: (() => Promise<void>) | undefined;\n const temporary = join(dirname(this.source!.path), `.codex-config-${randomUUID()}.tmp`);\n try {\n try {\n release = await lockfile.lock(this.source!.path, { realpath: false, retries: 0 });\n } catch {\n throw new AppError('CONFIG_BUSY', '\u914D\u7F6E\u6B63\u5728\u88AB\u5176\u4ED6\u64CD\u4F5C\u4FDD\u5B58\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002', 409);\n }\n const current = await this.snapshot();\n if (!current.writable) throw new AppError('CONFIG_READ_ONLY', current.readOnlyReason!, 409);\n if (current.revision !== parsed.revision)\n throw new AppError(\n 'CONFIG_CONFLICT',\n '\u914D\u7F6E\u6587\u4EF6\u5DF2\u88AB\u5176\u4ED6\u9875\u9762\u6216\u7A0B\u5E8F\u4FEE\u6539\u3002\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u518D\u4FDD\u5B58\u3002',\n 409,\n );\n if (\n config.dataDir !== current.saved.dataDir ||\n !same(config.projects, current.saved.projects)\n )\n throw new AppError(\n 'CONFIG_PROTECTED',\n '\u6570\u636E\u76EE\u5F55\u4E0D\u53EF\u5728\u6B64\u4FEE\u6539\uFF1B\u9879\u76EE\u8BF7\u4F7F\u7528\u9879\u76EE\u7BA1\u7406\u529F\u80FD\u3002',\n 400,\n );\n const contents = `${JSON.stringify(config, null, 2)}\\n`;\n if (Buffer.byteLength(contents) > MAX_CONFIG_BYTES)\n throw new AppError('INVALID_CONFIG', '\u914D\u7F6E\u6587\u4EF6\u4E0D\u80FD\u8D85\u8FC7 256 KiB\u3002');\n const file = await open(temporary, 'wx', 0o600);\n try {\n await file.writeFile(contents, 'utf8');\n await file.sync();\n } finally {\n await file.close();\n }\n // Catch external editors that changed the file while the temporary file was written.\n const latest = await this.snapshot();\n if (latest.revision !== current.revision || !latest.writable)\n throw new AppError('CONFIG_CONFLICT', '\u914D\u7F6E\u6587\u4EF6\u5DF2\u53D1\u751F\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u518D\u4FDD\u5B58\u3002', 409);\n await rename(temporary, this.source!.path);\n return await this.snapshot();\n } catch (error) {\n if (error instanceof AppError) throw error;\n throw new AppError(\n 'CONFIG_WRITE_FAILED',\n '\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u6743\u9650\u548C\u78C1\u76D8\u7A7A\u95F4\u540E\u91CD\u8BD5\u3002',\n 500,\n );\n } finally {\n await rm(temporary, { force: true });\n await release?.();\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,124 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { z } from "zod";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { DEFAULT_MAX_LINE_BYTES } from "../codex/transport.js";
6
+ const origin = z.string().regex(/^https?:\/\/[^/?#\\\s]+\/?$/i, "Expected an exact HTTP(S) origin").pipe(z.string().url()).transform((value, context) => {
7
+ const url = new URL(value);
8
+ if (!["http:", "https:"].includes(url.protocol) || url.hostname.includes("*") || value.includes("@") || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
9
+ context.addIssue({
10
+ code: "custom",
11
+ message: "Expected an exact HTTP(S) origin without wildcard, path, credentials, query or hash"
12
+ });
13
+ return z.NEVER;
14
+ }
15
+ return url.origin;
16
+ });
17
+ const project = z.object({
18
+ id: z.string().regex(/^[a-zA-Z0-9_-]+$/),
19
+ name: z.string().min(1),
20
+ path: z.string().refine(isAbsolute, "Project path must be absolute")
21
+ }).strict();
22
+ const configSchema = z.object({
23
+ host: z.string().default("127.0.0.1"),
24
+ port: z.number().int().min(1).max(65535).default(8787),
25
+ publicOrigin: z.array(origin).min(1).default(["http://127.0.0.1:8787"]).transform((values) => [...new Set(values)]),
26
+ developmentOrigins: z.array(origin).default([]),
27
+ dataDir: z.string().min(1).default("./.local/codex-web"),
28
+ codex: z.object({
29
+ binary: z.string().min(1).default("codex"),
30
+ home: z.string().refine(isAbsolute).optional(),
31
+ requestTimeoutMs: z.number().int().min(100).max(3e5).default(3e4),
32
+ maxLineBytes: z.number().int().min(1024).max(256 * 1024 * 1024).default(DEFAULT_MAX_LINE_BYTES)
33
+ }).strict().default({ binary: "codex", requestTimeoutMs: 3e4, maxLineBytes: DEFAULT_MAX_LINE_BYTES }),
34
+ auth: z.discriminatedUnion("mode", [
35
+ z.object({ mode: z.literal("local") }).strict(),
36
+ z.object({
37
+ mode: z.literal("cloudflare-access"),
38
+ issuer: z.string().url(),
39
+ audience: z.string().min(1)
40
+ }).strict()
41
+ ]).default({ mode: "local" }),
42
+ projects: z.array(project).default([]),
43
+ limits: z.object({
44
+ eventBytes: z.number().int().min(1024).max(64 * 1024 * 1024).default(16 * 1024 * 1024),
45
+ wsBytes: z.number().int().min(1024).max(16 * 1024 * 1024).default(4 * 1024 * 1024),
46
+ itemBytes: z.number().int().min(1024).max(8 * 1024 * 1024).default(2 * 1024 * 1024)
47
+ }).strict().default({
48
+ eventBytes: 16 * 1024 * 1024,
49
+ wsBytes: 4 * 1024 * 1024,
50
+ itemBytes: 2 * 1024 * 1024
51
+ })
52
+ }).strict();
53
+ const sources = /* @__PURE__ */ new WeakMap();
54
+ const configSource = (config) => sources.get(config);
55
+ function isLoopback(hostname) {
56
+ return ["127.0.0.1", "localhost", "::1", "[::1]"].includes(hostname);
57
+ }
58
+ function parseFileConfig(value) {
59
+ const parsed = configSchema.safeParse(value);
60
+ if (!parsed.success)
61
+ throw new AppError(
62
+ "INVALID_CONFIG",
63
+ parsed.error.issues.map((issue) => `${issue.path.join(".") || "\u914D\u7F6E"}: ${issue.message}`).join("\n")
64
+ );
65
+ const config = parsed.data;
66
+ if (new Set(config.projects.map((p) => p.id)).size !== config.projects.length)
67
+ throw new AppError("INVALID_CONFIG", "Project IDs must be unique");
68
+ if (config.auth.mode === "local" && !isLoopback(config.host))
69
+ throw new AppError("INVALID_CONFIG", "Local mode requires a loopback host");
70
+ if (config.developmentOrigins.some((o) => !isLoopback(new URL(o).hostname)))
71
+ throw new AppError("INVALID_CONFIG", "Development origins must be loopback");
72
+ if (config.auth.mode === "cloudflare-access") {
73
+ const issuer = new URL(config.auth.issuer);
74
+ if (issuer.protocol !== "https:" || !issuer.hostname.endsWith(".cloudflareaccess.com") || issuer.pathname !== "/" || issuer.search || issuer.hash || issuer.username || issuer.password)
75
+ throw new AppError(
76
+ "INVALID_CONFIG",
77
+ "Access issuer must be https://<team>.cloudflareaccess.com"
78
+ );
79
+ if (config.publicOrigin.some((value2) => new URL(value2).protocol !== "https:"))
80
+ throw new AppError("INVALID_CONFIG", "All Access public origins must use HTTPS");
81
+ config.auth.issuer = issuer.origin;
82
+ }
83
+ return config;
84
+ }
85
+ function parseConfig(value, filename, development = false) {
86
+ const config = parseFileConfig(value);
87
+ const result = {
88
+ ...config,
89
+ developmentOrigins: development ? config.developmentOrigins : [],
90
+ dataDir: resolve(dirname(filename), config.dataDir),
91
+ demo: false
92
+ };
93
+ sources.set(result, { path: resolve(filename), development });
94
+ return result;
95
+ }
96
+ function cliOptions(args = process.argv.slice(2)) {
97
+ const remaining = args.filter((arg) => arg !== "--");
98
+ let configPath = "config.local.json";
99
+ let development = false;
100
+ for (let i = 0; i < remaining.length; i++) {
101
+ const arg = remaining[i];
102
+ if (arg === "--dev") development = true;
103
+ else if (arg === "--config" && remaining[i + 1]) configPath = remaining[++i];
104
+ else throw new AppError("INVALID_ARGUMENT", `Unknown or incomplete argument: ${arg}`);
105
+ }
106
+ return { configPath: resolve(configPath), development };
107
+ }
108
+ async function loadConfig(options = cliOptions()) {
109
+ return parseConfig(
110
+ JSON.parse(await readFile(options.configPath, "utf8")),
111
+ options.configPath,
112
+ options.development
113
+ );
114
+ }
115
+ export {
116
+ cliOptions,
117
+ configSchema,
118
+ configSource,
119
+ isLoopback,
120
+ loadConfig,
121
+ parseConfig,
122
+ parseFileConfig
123
+ };
124
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/config.ts"],
4
+ "sourcesContent": ["import { readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\nimport { z } from 'zod';\nimport { AppError } from '../shared/errors.js';\nimport { DEFAULT_MAX_LINE_BYTES } from '../codex/transport.js';\nimport type { GatewayConfig } from '../shared/gateway-config.js';\nconst origin = z\n .string()\n .regex(/^https?:\\/\\/[^/?#\\\\\\s]+\\/?$/i, 'Expected an exact HTTP(S) origin')\n .pipe(z.string().url())\n .transform((value, context) => {\n const url = new URL(value);\n if (\n !['http:', 'https:'].includes(url.protocol) ||\n url.hostname.includes('*') ||\n value.includes('@') ||\n url.username ||\n url.password ||\n url.pathname !== '/' ||\n url.search ||\n url.hash\n ) {\n context.addIssue({\n code: 'custom',\n message:\n 'Expected an exact HTTP(S) origin without wildcard, path, credentials, query or hash',\n });\n return z.NEVER;\n }\n return url.origin;\n });\nconst project = z\n .object({\n id: z.string().regex(/^[a-zA-Z0-9_-]+$/),\n name: z.string().min(1),\n path: z.string().refine(isAbsolute, 'Project path must be absolute'),\n })\n .strict();\nexport const configSchema = z\n .object({\n host: z.string().default('127.0.0.1'),\n port: z.number().int().min(1).max(65535).default(8787),\n publicOrigin: z\n .array(origin)\n .min(1)\n .default(['http://127.0.0.1:8787'])\n .transform((values) => [...new Set(values)]),\n developmentOrigins: z.array(origin).default([]),\n dataDir: z.string().min(1).default('./.local/codex-web'),\n codex: z\n .object({\n binary: z.string().min(1).default('codex'),\n home: z.string().refine(isAbsolute).optional(),\n requestTimeoutMs: z.number().int().min(100).max(300_000).default(30_000),\n maxLineBytes: z\n .number()\n .int()\n .min(1024)\n .max(256 * 1024 * 1024)\n .default(DEFAULT_MAX_LINE_BYTES),\n })\n .strict()\n .default({ binary: 'codex', requestTimeoutMs: 30_000, maxLineBytes: DEFAULT_MAX_LINE_BYTES }),\n auth: z\n .discriminatedUnion('mode', [\n z.object({ mode: z.literal('local') }).strict(),\n z\n .object({\n mode: z.literal('cloudflare-access'),\n issuer: z.string().url(),\n audience: z.string().min(1),\n })\n .strict(),\n ])\n .default({ mode: 'local' }),\n projects: z.array(project).default([]),\n limits: z\n .object({\n eventBytes: z\n .number()\n .int()\n .min(1024)\n .max(64 * 1024 * 1024)\n .default(16 * 1024 * 1024),\n wsBytes: z\n .number()\n .int()\n .min(1024)\n .max(16 * 1024 * 1024)\n .default(4 * 1024 * 1024),\n itemBytes: z\n .number()\n .int()\n .min(1024)\n .max(8 * 1024 * 1024)\n .default(2 * 1024 * 1024),\n })\n .strict()\n .default({\n eventBytes: 16 * 1024 * 1024,\n wsBytes: 4 * 1024 * 1024,\n itemBytes: 2 * 1024 * 1024,\n }),\n })\n .strict();\nexport type AppConfig = z.infer<typeof configSchema> & { demo: boolean };\nconst sources = new WeakMap<AppConfig, { path: string; development: boolean }>();\nexport const configSource = (config: AppConfig) => sources.get(config);\nexport function isLoopback(hostname: string): boolean {\n return ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(hostname);\n}\nexport function parseFileConfig(value: unknown): GatewayConfig {\n const parsed = configSchema.safeParse(value);\n if (!parsed.success)\n throw new AppError(\n 'INVALID_CONFIG',\n parsed.error.issues\n .map((issue) => `${issue.path.join('.') || '\u914D\u7F6E'}: ${issue.message}`)\n .join('\\n'),\n );\n const config = parsed.data;\n if (new Set(config.projects.map((p) => p.id)).size !== config.projects.length)\n throw new AppError('INVALID_CONFIG', 'Project IDs must be unique');\n if (config.auth.mode === 'local' && !isLoopback(config.host))\n throw new AppError('INVALID_CONFIG', 'Local mode requires a loopback host');\n if (config.developmentOrigins.some((o) => !isLoopback(new URL(o).hostname)))\n throw new AppError('INVALID_CONFIG', 'Development origins must be loopback');\n if (config.auth.mode === 'cloudflare-access') {\n const issuer = new URL(config.auth.issuer);\n if (\n issuer.protocol !== 'https:' ||\n !issuer.hostname.endsWith('.cloudflareaccess.com') ||\n issuer.pathname !== '/' ||\n issuer.search ||\n issuer.hash ||\n issuer.username ||\n issuer.password\n )\n throw new AppError(\n 'INVALID_CONFIG',\n 'Access issuer must be https://<team>.cloudflareaccess.com',\n );\n if (config.publicOrigin.some((value) => new URL(value).protocol !== 'https:'))\n throw new AppError('INVALID_CONFIG', 'All Access public origins must use HTTPS');\n config.auth.issuer = issuer.origin;\n }\n return config;\n}\nexport function parseConfig(value: unknown, filename: string, development = false): AppConfig {\n const config = parseFileConfig(value);\n const result: AppConfig = {\n ...config,\n developmentOrigins: development ? config.developmentOrigins : [],\n dataDir: resolve(dirname(filename), config.dataDir),\n demo: false,\n };\n sources.set(result, { path: resolve(filename), development });\n return result;\n}\nexport function cliOptions(args = process.argv.slice(2)): {\n configPath: string;\n development: boolean;\n} {\n const remaining = args.filter((arg) => arg !== '--');\n let configPath = 'config.local.json';\n let development = false;\n for (let i = 0; i < remaining.length; i++) {\n const arg = remaining[i];\n if (arg === '--dev') development = true;\n else if (arg === '--config' && remaining[i + 1]) configPath = remaining[++i]!;\n else throw new AppError('INVALID_ARGUMENT', `Unknown or incomplete argument: ${arg}`);\n }\n return { configPath: resolve(configPath), development };\n}\nexport async function loadConfig(options = cliOptions()): Promise<AppConfig> {\n return parseConfig(\n JSON.parse(await readFile(options.configPath, 'utf8')),\n options.configPath,\n options.development,\n );\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["value"]
7
+ }
@@ -0,0 +1,21 @@
1
+ import { chmodSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { drizzle } from "drizzle-orm/better-sqlite3";
5
+ import * as schema from "./schema.js";
6
+ function openDatabase(filename) {
7
+ if (filename !== ":memory:") {
8
+ mkdirSync(dirname(filename), { recursive: true, mode: 448 });
9
+ chmodSync(dirname(filename), 448);
10
+ }
11
+ const sqlite = new Database(filename);
12
+ sqlite.pragma("foreign_keys = ON");
13
+ sqlite.pragma("journal_mode = WAL");
14
+ sqlite.pragma("busy_timeout = 5000");
15
+ if (filename !== ":memory:") chmodSync(filename, 384);
16
+ return { database: drizzle(sqlite, { schema }), close: () => sqlite.close() };
17
+ }
18
+ export {
19
+ openDatabase
20
+ };
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/server/db/index.ts"],
4
+ "sourcesContent": ["import { chmodSync, mkdirSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport Database from 'better-sqlite3';\nimport { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';\nimport * as schema from './schema.js';\n\nexport type AppDatabase = BetterSQLite3Database<typeof schema>;\n\n/** The caller must hold the data directory instance lock before opening/migrating. */\nexport function openDatabase(filename: string): { database: AppDatabase; close: () => void } {\n if (filename !== ':memory:') {\n mkdirSync(dirname(filename), { recursive: true, mode: 0o700 });\n chmodSync(dirname(filename), 0o700);\n }\n const sqlite = new Database(filename);\n sqlite.pragma('foreign_keys = ON');\n sqlite.pragma('journal_mode = WAL');\n sqlite.pragma('busy_timeout = 5000');\n if (filename !== ':memory:') chmodSync(filename, 0o600);\n return { database: drizzle(sqlite, { schema }), close: () => sqlite.close() };\n}\n"],
5
+ "mappings": "AAAA,SAAS,WAAW,iBAAiB;AACrC,SAAS,eAAe;AACxB,OAAO,cAAc;AACrB,SAAS,eAA2C;AACpD,YAAY,YAAY;AAKjB,SAAS,aAAa,UAAgE;AAC3F,MAAI,aAAa,YAAY;AAC3B,cAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAC7D,cAAU,QAAQ,QAAQ,GAAG,GAAK;AAAA,EACpC;AACA,QAAM,SAAS,IAAI,SAAS,QAAQ;AACpC,SAAO,OAAO,mBAAmB;AACjC,SAAO,OAAO,oBAAoB;AAClC,SAAO,OAAO,qBAAqB;AACnC,MAAI,aAAa,WAAY,WAAU,UAAU,GAAK;AACtD,SAAO,EAAE,UAAU,QAAQ,QAAQ,EAAE,OAAO,CAAC,GAAG,OAAO,MAAM,OAAO,MAAM,EAAE;AAC9E;",
6
+ "names": []
7
+ }
@@ -0,0 +1,26 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
3
+ import { readMigrationFiles } from "drizzle-orm/migrator";
4
+ import { sql } from "drizzle-orm";
5
+ const defaultMigrationsFolder = fileURLToPath(new URL("../../../drizzle", import.meta.url));
6
+ function migrateDatabase(database, migrationsFolder = defaultMigrationsFolder) {
7
+ const table = database.get(
8
+ sql`SELECT name FROM sqlite_master WHERE name = '__drizzle_migrations'`
9
+ );
10
+ if (table) {
11
+ const applied = database.all(
12
+ sql`SELECT hash FROM __drizzle_migrations ORDER BY created_at`
13
+ );
14
+ const migrations = readMigrationFiles({ migrationsFolder });
15
+ if (applied.some((entry, i) => entry.hash !== migrations[i]?.hash))
16
+ throw new Error(
17
+ "\u5F53\u524D\u5B89\u88C5\u4E0D\u517C\u5BB9\u6570\u636E\u5E93\u8FC1\u79FB\u5386\u53F2\uFF1B\u8BF7\u4F7F\u7528\u517C\u5BB9\u7248\u672C\u6216\u5347\u7EA7\u524D\u5907\u4EFD\uFF0C\u4E0D\u4F1A\u6539\u5199\u6216\u91CD\u5EFA\u6570\u636E\u5E93\u3002"
18
+ );
19
+ }
20
+ migrate(database, { migrationsFolder });
21
+ }
22
+ export {
23
+ defaultMigrationsFolder,
24
+ migrateDatabase
25
+ };
26
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/server/db/migrate.ts"],
4
+ "sourcesContent": ["import { fileURLToPath } from 'node:url';\nimport { migrate } from 'drizzle-orm/better-sqlite3/migrator';\nimport { readMigrationFiles } from 'drizzle-orm/migrator';\nimport { sql } from 'drizzle-orm';\nimport type { AppDatabase } from './index.js';\n\n/** Source and unbundled dist/server/db resolve to repository/dist drizzle respectively. */\nexport const defaultMigrationsFolder = fileURLToPath(new URL('../../../drizzle', import.meta.url));\n\nexport function migrateDatabase(\n database: AppDatabase,\n migrationsFolder = defaultMigrationsFolder,\n): void {\n const table = database.get(\n sql`SELECT name FROM sqlite_master WHERE name = '__drizzle_migrations'`,\n );\n if (table) {\n const applied = database.all<{ hash: string }>(\n sql`SELECT hash FROM __drizzle_migrations ORDER BY created_at`,\n );\n const migrations = readMigrationFiles({ migrationsFolder });\n if (applied.some((entry, i) => entry.hash !== migrations[i]?.hash))\n throw new Error(\n '\u5F53\u524D\u5B89\u88C5\u4E0D\u517C\u5BB9\u6570\u636E\u5E93\u8FC1\u79FB\u5386\u53F2\uFF1B\u8BF7\u4F7F\u7528\u517C\u5BB9\u7248\u672C\u6216\u5347\u7EA7\u524D\u5907\u4EFD\uFF0C\u4E0D\u4F1A\u6539\u5199\u6216\u91CD\u5EFA\u6570\u636E\u5E93\u3002',\n );\n }\n migrate(database, { migrationsFolder });\n}\n"],
5
+ "mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,0BAA0B;AACnC,SAAS,WAAW;AAIb,MAAM,0BAA0B,cAAc,IAAI,IAAI,oBAAoB,YAAY,GAAG,CAAC;AAE1F,SAAS,gBACd,UACA,mBAAmB,yBACb;AACN,QAAM,QAAQ,SAAS;AAAA,IACrB;AAAA,EACF;AACA,MAAI,OAAO;AACT,UAAM,UAAU,SAAS;AAAA,MACvB;AAAA,IACF;AACA,UAAM,aAAa,mBAAmB,EAAE,iBAAiB,CAAC;AAC1D,QAAI,QAAQ,KAAK,CAAC,OAAO,MAAM,MAAM,SAAS,WAAW,CAAC,GAAG,IAAI;AAC/D,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,EACJ;AACA,UAAQ,UAAU,EAAE,iBAAiB,CAAC;AACxC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,201 @@
1
+ import { createHash } from "node:crypto";
2
+ import { and, eq, isNull, or } from "drizzle-orm";
3
+ import { executionMarkers, operations, threadLinks } from "./schema.js";
4
+ class StoreConflict extends Error {
5
+ constructor(code, message, threadId) {
6
+ super(message);
7
+ this.code = code;
8
+ this.threadId = threadId;
9
+ }
10
+ code;
11
+ threadId;
12
+ }
13
+ function canonical(value) {
14
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
15
+ if (value !== null && typeof value === "object") {
16
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
17
+ }
18
+ return JSON.stringify(value) ?? "null";
19
+ }
20
+ function requestDigest(value) {
21
+ return createHash("sha256").update(canonical(value)).digest("hex");
22
+ }
23
+ class OperationStore {
24
+ constructor(database) {
25
+ this.database = database;
26
+ }
27
+ database;
28
+ accept(clientOperationId, kind, target, request) {
29
+ const digest = requestDigest(request);
30
+ return this.database.transaction((tx) => {
31
+ const now = Date.now();
32
+ const inserted = tx.insert(operations).values({
33
+ clientOperationId,
34
+ kind,
35
+ target,
36
+ requestDigest: digest,
37
+ state: "accepted",
38
+ createdAt: now,
39
+ updatedAt: now
40
+ }).onConflictDoNothing({ target: operations.clientOperationId }).run().changes === 1;
41
+ const operation = tx.select().from(operations).where(eq(operations.clientOperationId, clientOperationId)).get();
42
+ if (!operation) throw new Error("Operation insert did not persist");
43
+ if (operation.kind !== kind || operation.target !== target || operation.requestDigest !== digest) {
44
+ throw new StoreConflict("OPERATION_CONFLICT", "\u540C\u4E00\u64CD\u4F5C ID \u5DF2\u7528\u4E8E\u4E0D\u540C\u7684\u8BF7\u6C42");
45
+ }
46
+ return { operation, inserted };
47
+ });
48
+ }
49
+ get(id) {
50
+ return this.database.select().from(operations).where(eq(operations.clientOperationId, id)).get();
51
+ }
52
+ associateProject(id, projectId) {
53
+ this.database.update(operations).set({ projectId }).where(and(eq(operations.clientOperationId, id), eq(operations.state, "accepted"))).run();
54
+ }
55
+ /** Includes older rows whose project can be recovered from their stable target/link. */
56
+ pendingProjectOperations(projectId) {
57
+ return this.database.select().from(operations).where(
58
+ or(
59
+ eq(operations.state, "accepted"),
60
+ eq(operations.state, "dispatching"),
61
+ and(eq(operations.state, "unknown"), isNull(operations.reconciledAt))
62
+ )
63
+ ).all().filter((operation) => {
64
+ if (![
65
+ "thread.create",
66
+ "thread.resume",
67
+ "thread.fork",
68
+ "thread.archive",
69
+ "thread.rename"
70
+ ].includes(operation.kind))
71
+ return false;
72
+ const owner = operation.projectId ?? (operation.kind === "thread.create" ? operation.target : this.getThreadLink(operation.target)?.projectId);
73
+ return owner === projectId;
74
+ });
75
+ }
76
+ dispatch(id, marker) {
77
+ this.database.transaction((tx) => {
78
+ if (marker) {
79
+ const occupied = tx.select().from(executionMarkers).where(
80
+ and(isNull(executionMarkers.endedAt), eq(executionMarkers.threadId, marker.threadId))
81
+ ).get();
82
+ if (occupied)
83
+ throw new StoreConflict(
84
+ "THREAD_BUSY",
85
+ "\u8BE5\u4F1A\u8BDD\u7684\u4E0A\u4E00\u8F6E\u6267\u884C\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u8FFD\u52A0\u8981\u6C42\u6216\u7B49\u5F85\u7ED3\u675F",
86
+ occupied.threadId
87
+ );
88
+ }
89
+ const updated = tx.update(operations).set({ state: "dispatching", updatedAt: Date.now() }).where(and(eq(operations.clientOperationId, id), eq(operations.state, "accepted"))).run();
90
+ if (updated.changes !== 1)
91
+ throw new StoreConflict("OPERATION_CONFLICT", "\u64CD\u4F5C\u5DF2\u7ECF\u6D3E\u53D1\u6216\u7ED3\u675F");
92
+ if (marker)
93
+ tx.insert(executionMarkers).values({ operationId: id, ...marker, state: "active", createdAt: Date.now() }).run();
94
+ });
95
+ }
96
+ acknowledge(id, result, turnId) {
97
+ this.database.transaction((tx) => {
98
+ tx.update(operations).set({ state: "acknowledged", result, error: null, updatedAt: Date.now() }).where(and(eq(operations.clientOperationId, id), eq(operations.state, "dispatching"))).run();
99
+ if (turnId !== void 0)
100
+ tx.update(executionMarkers).set({ turnId }).where(eq(executionMarkers.operationId, id)).run();
101
+ });
102
+ return this.get(id);
103
+ }
104
+ fail(id, error, uncertain = false) {
105
+ this.database.transaction((tx) => {
106
+ const changed = tx.update(operations).set({ state: uncertain ? "unknown" : "failed", error, updatedAt: Date.now() }).where(
107
+ and(
108
+ eq(operations.clientOperationId, id),
109
+ or(eq(operations.state, "accepted"), eq(operations.state, "dispatching"))
110
+ )
111
+ ).run();
112
+ if (changed.changes === 0) return;
113
+ tx.update(executionMarkers).set(uncertain ? { state: "unknown" } : { state: "terminal", endedAt: Date.now() }).where(and(eq(executionMarkers.operationId, id), isNull(executionMarkers.endedAt))).run();
114
+ });
115
+ return this.get(id);
116
+ }
117
+ /** Includes acknowledged requests with unfinished turns; none of these are retried. */
118
+ recover() {
119
+ this.database.transaction((tx) => {
120
+ tx.update(operations).set({
121
+ state: "failed",
122
+ error: { code: "NOT_DISPATCHED", message: "\u670D\u52A1\u5728\u6D3E\u53D1\u524D\u505C\u6B62\uFF1B\u64CD\u4F5C\u6CA1\u6709\u6267\u884C" },
123
+ updatedAt: Date.now()
124
+ }).where(eq(operations.state, "accepted")).run();
125
+ tx.update(operations).set({
126
+ state: "unknown",
127
+ error: { code: "RESULT_UNKNOWN", message: "\u8FD0\u884C\u65F6\u5DF2\u505C\u6B62\uFF0C\u63D0\u4EA4\u7ED3\u679C\u9700\u8981\u6838\u5BF9" },
128
+ updatedAt: Date.now()
129
+ }).where(eq(operations.state, "dispatching")).run();
130
+ tx.update(executionMarkers).set({ state: "unknown" }).where(isNull(executionMarkers.endedAt)).run();
131
+ });
132
+ }
133
+ activeMarkers() {
134
+ return this.database.select().from(executionMarkers).where(isNull(executionMarkers.endedAt)).all();
135
+ }
136
+ unresolvedOperations() {
137
+ return this.database.select().from(operations).where(
138
+ or(
139
+ eq(operations.state, "accepted"),
140
+ eq(operations.state, "dispatching"),
141
+ and(eq(operations.state, "unknown"), isNull(operations.reconciledAt))
142
+ )
143
+ ).all();
144
+ }
145
+ marker(operationId) {
146
+ return this.database.select().from(executionMarkers).where(eq(executionMarkers.operationId, operationId)).get();
147
+ }
148
+ activeForThread(threadId) {
149
+ return this.database.select().from(executionMarkers).where(and(eq(executionMarkers.threadId, threadId), isNull(executionMarkers.endedAt))).get();
150
+ }
151
+ attachTurn(operationId, turnId) {
152
+ this.database.update(executionMarkers).set({ turnId }).where(and(eq(executionMarkers.operationId, operationId), isNull(executionMarkers.endedAt))).run();
153
+ }
154
+ finishTurn(threadId, turnId, codexEpoch) {
155
+ this.database.update(executionMarkers).set({ state: "terminal", endedAt: Date.now() }).where(
156
+ and(
157
+ eq(executionMarkers.threadId, threadId),
158
+ eq(executionMarkers.turnId, turnId),
159
+ eq(executionMarkers.codexEpoch, codexEpoch),
160
+ isNull(executionMarkers.endedAt)
161
+ )
162
+ ).run();
163
+ }
164
+ /** Keys the release to the original execution; an old decision cannot unlock a later turn. */
165
+ reconcile(targetOperationId, reconciliationOperationId) {
166
+ this.database.transaction((tx) => {
167
+ const now = Date.now();
168
+ tx.update(operations).set({ reconciledAt: now, reconciliationDecision: "confirmed-ended" }).where(
169
+ and(eq(operations.clientOperationId, targetOperationId), isNull(operations.reconciledAt))
170
+ ).run();
171
+ tx.update(executionMarkers).set({ state: "reconciled", endedAt: now, reconciledAt: now, reconciliationOperationId }).where(
172
+ and(
173
+ eq(executionMarkers.operationId, targetOperationId),
174
+ isNull(executionMarkers.endedAt)
175
+ )
176
+ ).run();
177
+ tx.update(operations).set({
178
+ state: "acknowledged",
179
+ result: { targetOperationId, decision: "confirmed-ended", reconciledAt: now },
180
+ updatedAt: now
181
+ }).where(
182
+ and(
183
+ eq(operations.clientOperationId, reconciliationOperationId),
184
+ eq(operations.state, "dispatching")
185
+ )
186
+ ).run();
187
+ });
188
+ }
189
+ linkThread(link) {
190
+ this.database.insert(threadLinks).values({ ...link, updatedAt: Date.now() }).onConflictDoUpdate({ target: threadLinks.threadId, set: { ...link, updatedAt: Date.now() } }).run();
191
+ }
192
+ getThreadLink(threadId) {
193
+ return this.database.select().from(threadLinks).where(eq(threadLinks.threadId, threadId)).get();
194
+ }
195
+ }
196
+ export {
197
+ OperationStore,
198
+ StoreConflict,
199
+ requestDigest
200
+ };
201
+ //# sourceMappingURL=operations.js.map