@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.
- package/LICENSE +21 -0
- package/README.md +173 -2
- package/README.zh-CN.md +174 -0
- package/bin/covro.mjs +8 -0
- package/dist/cli/control.js +66 -0
- package/dist/cli/control.js.map +7 -0
- package/dist/cli/daemon.js +118 -0
- package/dist/cli/daemon.js.map +7 -0
- package/dist/cli/main.js +152 -0
- package/dist/cli/main.js.map +7 -0
- package/dist/cli/paths.js +60 -0
- package/dist/cli/paths.js.map +7 -0
- package/dist/cli/service.js +87 -0
- package/dist/cli/service.js.map +7 -0
- package/dist/codex/adapter.js +1195 -0
- package/dist/codex/adapter.js.map +7 -0
- package/dist/codex/auto-review.js +102 -0
- package/dist/codex/auto-review.js.map +7 -0
- package/dist/codex/compatibility.js +7 -0
- package/dist/codex/compatibility.js.map +7 -0
- package/dist/codex/mapping.js +210 -0
- package/dist/codex/mapping.js.map +7 -0
- package/dist/codex/permissions.js +51 -0
- package/dist/codex/permissions.js.map +7 -0
- package/dist/codex/thread-title-policy.js +52 -0
- package/dist/codex/thread-title-policy.js.map +7 -0
- package/dist/codex/thread-titles.js +259 -0
- package/dist/codex/thread-titles.js.map +7 -0
- package/dist/codex/transport.js +236 -0
- package/dist/codex/transport.js.map +7 -0
- package/dist/codex/turn-token-usage.js +46 -0
- package/dist/codex/turn-token-usage.js.map +7 -0
- package/dist/codex/types.js +1 -0
- package/dist/codex/types.js.map +7 -0
- package/dist/drizzle/0000_past_wildside.sql +45 -0
- package/dist/drizzle/0001_shocking_ender_wiggin.sql +2 -0
- package/dist/drizzle/0002_allow_parallel_threads.sql +1 -0
- package/dist/drizzle/0003_project_registrations.sql +11 -0
- package/dist/drizzle/0004_turn_usage.sql +6 -0
- package/dist/drizzle/meta/0000_snapshot.json +309 -0
- package/dist/drizzle/meta/0001_snapshot.json +323 -0
- package/dist/drizzle/meta/0002_snapshot.json +315 -0
- package/dist/drizzle/meta/0003_snapshot.json +381 -0
- package/dist/drizzle/meta/0004_snapshot.json +420 -0
- package/dist/drizzle/meta/_journal.json +41 -0
- package/dist/server/auth.js +47 -0
- package/dist/server/auth.js.map +7 -0
- package/dist/server/compaction.js +40 -0
- package/dist/server/compaction.js.map +7 -0
- package/dist/server/config-store.js +147 -0
- package/dist/server/config-store.js.map +7 -0
- package/dist/server/config.js +124 -0
- package/dist/server/config.js.map +7 -0
- package/dist/server/db/index.js +21 -0
- package/dist/server/db/index.js.map +7 -0
- package/dist/server/db/migrate.js +26 -0
- package/dist/server/db/migrate.js.map +7 -0
- package/dist/server/db/operations.js +201 -0
- package/dist/server/db/operations.js.map +7 -0
- package/dist/server/db/schema.js +99 -0
- package/dist/server/db/schema.js.map +7 -0
- package/dist/server/db/turn-usage.js +31 -0
- package/dist/server/db/turn-usage.js.map +7 -0
- package/dist/server/events.js +123 -0
- package/dist/server/events.js.map +7 -0
- package/dist/server/http.js +305 -0
- package/dist/server/http.js.map +7 -0
- package/dist/server/images.js +49 -0
- package/dist/server/images.js.map +7 -0
- package/dist/server/instance-lock.js +25 -0
- package/dist/server/instance-lock.js.map +7 -0
- package/dist/server/local-files.js +100 -0
- package/dist/server/local-files.js.map +7 -0
- package/dist/server/main.js +125 -0
- package/dist/server/main.js.map +7 -0
- package/dist/server/markdown.js +28 -0
- package/dist/server/markdown.js.map +7 -0
- package/dist/server/projection.js +348 -0
- package/dist/server/projection.js.map +7 -0
- package/dist/server/projects.js +230 -0
- package/dist/server/projects.js.map +7 -0
- package/dist/server/service.js +1221 -0
- package/dist/server/service.js.map +7 -0
- package/dist/server/snapshot.js +98 -0
- package/dist/server/snapshot.js.map +7 -0
- package/dist/server/thread-export.js +97 -0
- package/dist/server/thread-export.js.map +7 -0
- package/dist/server/websocket.js +126 -0
- package/dist/server/websocket.js.map +7 -0
- package/dist/shared/async-questions.js +39 -0
- package/dist/shared/async-questions.js.map +7 -0
- package/dist/shared/codex-compatibility.js +56 -0
- package/dist/shared/codex-compatibility.js.map +7 -0
- package/dist/shared/elicitation.js +314 -0
- package/dist/shared/elicitation.js.map +7 -0
- package/dist/shared/errors.js +24 -0
- package/dist/shared/errors.js.map +7 -0
- package/dist/shared/gateway-config.js +1 -0
- package/dist/shared/gateway-config.js.map +7 -0
- package/dist/shared/images.js +21 -0
- package/dist/shared/images.js.map +7 -0
- package/dist/shared/package.js +5 -0
- package/dist/shared/package.js.map +7 -0
- package/dist/shared/permissions.js +63 -0
- package/dist/shared/permissions.js.map +7 -0
- package/dist/shared/thread-name.js +14 -0
- package/dist/shared/thread-name.js.map +7 -0
- package/dist/shared/thread-summary.js +24 -0
- package/dist/shared/thread-summary.js.map +7 -0
- package/dist/shared/types.js +1 -0
- package/dist/shared/types.js.map +7 -0
- package/dist/shared/updates.js +7 -0
- package/dist/shared/updates.js.map +7 -0
- package/dist/shared/validation.js +116 -0
- package/dist/shared/validation.js.map +7 -0
- package/dist/shared/versions.js +36 -0
- package/dist/shared/versions.js.map +7 -0
- package/dist/update-worker.cjs +21968 -0
- package/dist/updates/files.js +54 -0
- package/dist/updates/files.js.map +7 -0
- package/dist/updates/manager.js +315 -0
- package/dist/updates/manager.js.map +7 -0
- package/dist/updates/registry.js +71 -0
- package/dist/updates/registry.js.map +7 -0
- package/dist/updates/transaction.js +96 -0
- package/dist/updates/transaction.js.map +7 -0
- package/dist/updates/worker.js +345 -0
- package/dist/updates/worker.js.map +7 -0
- package/dist/web/assets/index-BW9afH_Y.js +83 -0
- package/dist/web/assets/index-L3S1ZiO0.css +1 -0
- package/dist/web/covro.svg +6 -0
- package/dist/web/index.html +15 -0
- package/package.json +88 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/server/db/operations.ts"],
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto';\nimport { and, eq, isNull, or } from 'drizzle-orm';\nimport type { AppDatabase } from './index.js';\nimport { executionMarkers, operations, threadLinks } from './schema.js';\nimport type { ApiError } from '../../shared/types.js';\n\nexport type OperationRow = typeof operations.$inferSelect;\nexport type ExecutionMarker = typeof executionMarkers.$inferSelect;\nexport type ThreadLink = typeof threadLinks.$inferSelect;\n\nexport class StoreConflict extends Error {\n constructor(\n public readonly code: 'OPERATION_CONFLICT' | 'THREAD_BUSY',\n message: string,\n public readonly threadId?: string,\n ) {\n super(message);\n }\n}\n\nfunction canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value !== null && typeof value === 'object') {\n return `{${Object.entries(value)\n .filter(([, item]) => item !== undefined)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`)\n .join(',')}}`;\n }\n return JSON.stringify(value) ?? 'null';\n}\n\nexport function requestDigest(value: unknown): string {\n return createHash('sha256').update(canonical(value)).digest('hex');\n}\n\n/** Every transaction is synchronous; no process/network work occurs inside one. */\nexport class OperationStore {\n constructor(public readonly database: AppDatabase) {}\n\n accept(\n clientOperationId: string,\n kind: string,\n target: string,\n request: unknown,\n ): { operation: OperationRow; inserted: boolean } {\n const digest = requestDigest(request);\n return this.database.transaction((tx) => {\n const now = Date.now();\n const inserted =\n tx\n .insert(operations)\n .values({\n clientOperationId,\n kind,\n target,\n requestDigest: digest,\n state: 'accepted',\n createdAt: now,\n updatedAt: now,\n })\n .onConflictDoNothing({ target: operations.clientOperationId })\n .run().changes === 1;\n const operation = tx\n .select()\n .from(operations)\n .where(eq(operations.clientOperationId, clientOperationId))\n .get();\n if (!operation) throw new Error('Operation insert did not persist');\n if (\n operation.kind !== kind ||\n operation.target !== target ||\n operation.requestDigest !== digest\n ) {\n throw new StoreConflict('OPERATION_CONFLICT', '\u540C\u4E00\u64CD\u4F5C ID \u5DF2\u7528\u4E8E\u4E0D\u540C\u7684\u8BF7\u6C42');\n }\n return { operation, inserted };\n });\n }\n\n get(id: string): OperationRow | undefined {\n return this.database\n .select()\n .from(operations)\n .where(eq(operations.clientOperationId, id))\n .get();\n }\n\n associateProject(id: string, projectId: string): void {\n this.database\n .update(operations)\n .set({ projectId })\n .where(and(eq(operations.clientOperationId, id), eq(operations.state, 'accepted')))\n .run();\n }\n\n /** Includes older rows whose project can be recovered from their stable target/link. */\n pendingProjectOperations(projectId: string): OperationRow[] {\n return this.database\n .select()\n .from(operations)\n .where(\n or(\n eq(operations.state, 'accepted'),\n eq(operations.state, 'dispatching'),\n and(eq(operations.state, 'unknown'), isNull(operations.reconciledAt)),\n ),\n )\n .all()\n .filter((operation) => {\n if (\n ![\n 'thread.create',\n 'thread.resume',\n 'thread.fork',\n 'thread.archive',\n 'thread.rename',\n ].includes(operation.kind)\n )\n return false;\n const owner =\n operation.projectId ??\n (operation.kind === 'thread.create'\n ? operation.target\n : this.getThreadLink(operation.target)?.projectId);\n return owner === projectId;\n });\n }\n\n dispatch(\n id: string,\n marker?: { threadId: string; workspaceKey: string; codexEpoch: string },\n ): void {\n this.database.transaction((tx) => {\n if (marker) {\n const occupied = tx\n .select()\n .from(executionMarkers)\n .where(\n and(isNull(executionMarkers.endedAt), eq(executionMarkers.threadId, marker.threadId)),\n )\n .get();\n if (occupied)\n throw new StoreConflict(\n 'THREAD_BUSY',\n '\u8BE5\u4F1A\u8BDD\u7684\u4E0A\u4E00\u8F6E\u6267\u884C\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u8FFD\u52A0\u8981\u6C42\u6216\u7B49\u5F85\u7ED3\u675F',\n occupied.threadId,\n );\n }\n const updated = tx\n .update(operations)\n .set({ state: 'dispatching', updatedAt: Date.now() })\n .where(and(eq(operations.clientOperationId, id), eq(operations.state, 'accepted')))\n .run();\n if (updated.changes !== 1)\n throw new StoreConflict('OPERATION_CONFLICT', '\u64CD\u4F5C\u5DF2\u7ECF\u6D3E\u53D1\u6216\u7ED3\u675F');\n if (marker)\n tx.insert(executionMarkers)\n .values({ operationId: id, ...marker, state: 'active', createdAt: Date.now() })\n .run();\n });\n }\n\n acknowledge(id: string, result: unknown, turnId?: string): OperationRow {\n this.database.transaction((tx) => {\n tx.update(operations)\n .set({ state: 'acknowledged', result, error: null, updatedAt: Date.now() })\n .where(and(eq(operations.clientOperationId, id), eq(operations.state, 'dispatching')))\n .run();\n if (turnId !== undefined)\n tx.update(executionMarkers)\n .set({ turnId })\n .where(eq(executionMarkers.operationId, id))\n .run();\n });\n return this.get(id)!;\n }\n\n fail(id: string, error: ApiError, uncertain = false): OperationRow {\n this.database.transaction((tx) => {\n const changed = tx\n .update(operations)\n .set({ state: uncertain ? 'unknown' : 'failed', error, updatedAt: Date.now() })\n .where(\n and(\n eq(operations.clientOperationId, id),\n or(eq(operations.state, 'accepted'), eq(operations.state, 'dispatching')),\n ),\n )\n .run();\n // A late rejection must not turn an already-recovered unknown execution into a release.\n if (changed.changes === 0) return;\n tx.update(executionMarkers)\n .set(uncertain ? { state: 'unknown' } : { state: 'terminal', endedAt: Date.now() })\n .where(and(eq(executionMarkers.operationId, id), isNull(executionMarkers.endedAt)))\n .run();\n });\n return this.get(id)!;\n }\n\n /** Includes acknowledged requests with unfinished turns; none of these are retried. */\n recover(): void {\n this.database.transaction((tx) => {\n tx.update(operations)\n .set({\n state: 'failed',\n error: { code: 'NOT_DISPATCHED', message: '\u670D\u52A1\u5728\u6D3E\u53D1\u524D\u505C\u6B62\uFF1B\u64CD\u4F5C\u6CA1\u6709\u6267\u884C' },\n updatedAt: Date.now(),\n })\n .where(eq(operations.state, 'accepted'))\n .run();\n tx.update(operations)\n .set({\n state: 'unknown',\n error: { code: 'RESULT_UNKNOWN', message: '\u8FD0\u884C\u65F6\u5DF2\u505C\u6B62\uFF0C\u63D0\u4EA4\u7ED3\u679C\u9700\u8981\u6838\u5BF9' },\n updatedAt: Date.now(),\n })\n .where(eq(operations.state, 'dispatching'))\n .run();\n tx.update(executionMarkers)\n .set({ state: 'unknown' })\n .where(isNull(executionMarkers.endedAt))\n .run();\n });\n }\n\n activeMarkers(): ExecutionMarker[] {\n return this.database\n .select()\n .from(executionMarkers)\n .where(isNull(executionMarkers.endedAt))\n .all();\n }\n\n unresolvedOperations(): OperationRow[] {\n return this.database\n .select()\n .from(operations)\n .where(\n or(\n eq(operations.state, 'accepted'),\n eq(operations.state, 'dispatching'),\n and(eq(operations.state, 'unknown'), isNull(operations.reconciledAt)),\n ),\n )\n .all();\n }\n\n marker(operationId: string): ExecutionMarker | undefined {\n return this.database\n .select()\n .from(executionMarkers)\n .where(eq(executionMarkers.operationId, operationId))\n .get();\n }\n\n activeForThread(threadId: string): ExecutionMarker | undefined {\n return this.database\n .select()\n .from(executionMarkers)\n .where(and(eq(executionMarkers.threadId, threadId), isNull(executionMarkers.endedAt)))\n .get();\n }\n\n attachTurn(operationId: string, turnId: string): void {\n this.database\n .update(executionMarkers)\n .set({ turnId })\n .where(and(eq(executionMarkers.operationId, operationId), isNull(executionMarkers.endedAt)))\n .run();\n }\n\n finishTurn(threadId: string, turnId: string, codexEpoch: string): void {\n this.database\n .update(executionMarkers)\n .set({ state: 'terminal', endedAt: Date.now() })\n .where(\n and(\n eq(executionMarkers.threadId, threadId),\n eq(executionMarkers.turnId, turnId),\n eq(executionMarkers.codexEpoch, codexEpoch),\n isNull(executionMarkers.endedAt),\n ),\n )\n .run();\n }\n\n /** Keys the release to the original execution; an old decision cannot unlock a later turn. */\n reconcile(targetOperationId: string, reconciliationOperationId: string): void {\n this.database.transaction((tx) => {\n const now = Date.now();\n tx.update(operations)\n .set({ reconciledAt: now, reconciliationDecision: 'confirmed-ended' })\n .where(\n and(eq(operations.clientOperationId, targetOperationId), isNull(operations.reconciledAt)),\n )\n .run();\n tx.update(executionMarkers)\n .set({ state: 'reconciled', endedAt: now, reconciledAt: now, reconciliationOperationId })\n .where(\n and(\n eq(executionMarkers.operationId, targetOperationId),\n isNull(executionMarkers.endedAt),\n ),\n )\n .run();\n tx.update(operations)\n .set({\n state: 'acknowledged',\n result: { targetOperationId, decision: 'confirmed-ended', reconciledAt: now },\n updatedAt: now,\n })\n .where(\n and(\n eq(operations.clientOperationId, reconciliationOperationId),\n eq(operations.state, 'dispatching'),\n ),\n )\n .run();\n });\n }\n\n linkThread(link: Omit<ThreadLink, 'updatedAt'>): void {\n this.database\n .insert(threadLinks)\n .values({ ...link, updatedAt: Date.now() })\n .onConflictDoUpdate({ target: threadLinks.threadId, set: { ...link, updatedAt: Date.now() } })\n .run();\n }\n\n getThreadLink(threadId: string): ThreadLink | undefined {\n return this.database.select().from(threadLinks).where(eq(threadLinks.threadId, threadId)).get();\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AAC3B,SAAS,KAAK,IAAI,QAAQ,UAAU;AAEpC,SAAS,kBAAkB,YAAY,mBAAmB;AAOnD,MAAM,sBAAsB,MAAM;AAAA,EACvC,YACkB,MAChB,SACgB,UAChB;AACA,UAAM,OAAO;AAJG;AAEA;AAAA,EAGlB;AAAA,EALkB;AAAA,EAEA;AAIpB;AAEA,SAAS,UAAU,OAAwB;AACzC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,IAAI,MAAM,IAAI,SAAS,EAAE,KAAK,GAAG,CAAC;AACnE,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,WAAO,IAAI,OAAO,QAAQ,KAAK,EAC5B,OAAO,CAAC,CAAC,EAAE,IAAI,MAAM,SAAS,MAAS,EACvC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,EACrC,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,GAAG,KAAK,UAAU,GAAG,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE,EAChE,KAAK,GAAG,CAAC;AAAA,EACd;AACA,SAAO,KAAK,UAAU,KAAK,KAAK;AAClC;AAEO,SAAS,cAAc,OAAwB;AACpD,SAAO,WAAW,QAAQ,EAAE,OAAO,UAAU,KAAK,CAAC,EAAE,OAAO,KAAK;AACnE;AAGO,MAAM,eAAe;AAAA,EAC1B,YAA4B,UAAuB;AAAvB;AAAA,EAAwB;AAAA,EAAxB;AAAA,EAE5B,OACE,mBACA,MACA,QACA,SACgD;AAChD,UAAM,SAAS,cAAc,OAAO;AACpC,WAAO,KAAK,SAAS,YAAY,CAAC,OAAO;AACvC,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,WACJ,GACG,OAAO,UAAU,EACjB,OAAO;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA,eAAe;AAAA,QACf,OAAO;AAAA,QACP,WAAW;AAAA,QACX,WAAW;AAAA,MACb,CAAC,EACA,oBAAoB,EAAE,QAAQ,WAAW,kBAAkB,CAAC,EAC5D,IAAI,EAAE,YAAY;AACvB,YAAM,YAAY,GACf,OAAO,EACP,KAAK,UAAU,EACf,MAAM,GAAG,WAAW,mBAAmB,iBAAiB,CAAC,EACzD,IAAI;AACP,UAAI,CAAC,UAAW,OAAM,IAAI,MAAM,kCAAkC;AAClE,UACE,UAAU,SAAS,QACnB,UAAU,WAAW,UACrB,UAAU,kBAAkB,QAC5B;AACA,cAAM,IAAI,cAAc,sBAAsB,8EAAkB;AAAA,MAClE;AACA,aAAO,EAAE,WAAW,SAAS;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,IAAsC;AACxC,WAAO,KAAK,SACT,OAAO,EACP,KAAK,UAAU,EACf,MAAM,GAAG,WAAW,mBAAmB,EAAE,CAAC,EAC1C,IAAI;AAAA,EACT;AAAA,EAEA,iBAAiB,IAAY,WAAyB;AACpD,SAAK,SACF,OAAO,UAAU,EACjB,IAAI,EAAE,UAAU,CAAC,EACjB,MAAM,IAAI,GAAG,WAAW,mBAAmB,EAAE,GAAG,GAAG,WAAW,OAAO,UAAU,CAAC,CAAC,EACjF,IAAI;AAAA,EACT;AAAA;AAAA,EAGA,yBAAyB,WAAmC;AAC1D,WAAO,KAAK,SACT,OAAO,EACP,KAAK,UAAU,EACf;AAAA,MACC;AAAA,QACE,GAAG,WAAW,OAAO,UAAU;AAAA,QAC/B,GAAG,WAAW,OAAO,aAAa;AAAA,QAClC,IAAI,GAAG,WAAW,OAAO,SAAS,GAAG,OAAO,WAAW,YAAY,CAAC;AAAA,MACtE;AAAA,IACF,EACC,IAAI,EACJ,OAAO,CAAC,cAAc;AACrB,UACE,CAAC;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,SAAS,UAAU,IAAI;AAEzB,eAAO;AACT,YAAM,QACJ,UAAU,cACT,UAAU,SAAS,kBAChB,UAAU,SACV,KAAK,cAAc,UAAU,MAAM,GAAG;AAC5C,aAAO,UAAU;AAAA,IACnB,CAAC;AAAA,EACL;AAAA,EAEA,SACE,IACA,QACM;AACN,SAAK,SAAS,YAAY,CAAC,OAAO;AAChC,UAAI,QAAQ;AACV,cAAM,WAAW,GACd,OAAO,EACP,KAAK,gBAAgB,EACrB;AAAA,UACC,IAAI,OAAO,iBAAiB,OAAO,GAAG,GAAG,iBAAiB,UAAU,OAAO,QAAQ,CAAC;AAAA,QACtF,EACC,IAAI;AACP,YAAI;AACF,gBAAM,IAAI;AAAA,YACR;AAAA,YACA;AAAA,YACA,SAAS;AAAA,UACX;AAAA,MACJ;AACA,YAAM,UAAU,GACb,OAAO,UAAU,EACjB,IAAI,EAAE,OAAO,eAAe,WAAW,KAAK,IAAI,EAAE,CAAC,EACnD,MAAM,IAAI,GAAG,WAAW,mBAAmB,EAAE,GAAG,GAAG,WAAW,OAAO,UAAU,CAAC,CAAC,EACjF,IAAI;AACP,UAAI,QAAQ,YAAY;AACtB,cAAM,IAAI,cAAc,sBAAsB,wDAAW;AAC3D,UAAI;AACF,WAAG,OAAO,gBAAgB,EACvB,OAAO,EAAE,aAAa,IAAI,GAAG,QAAQ,OAAO,UAAU,WAAW,KAAK,IAAI,EAAE,CAAC,EAC7E,IAAI;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEA,YAAY,IAAY,QAAiB,QAA+B;AACtE,SAAK,SAAS,YAAY,CAAC,OAAO;AAChC,SAAG,OAAO,UAAU,EACjB,IAAI,EAAE,OAAO,gBAAgB,QAAQ,OAAO,MAAM,WAAW,KAAK,IAAI,EAAE,CAAC,EACzE,MAAM,IAAI,GAAG,WAAW,mBAAmB,EAAE,GAAG,GAAG,WAAW,OAAO,aAAa,CAAC,CAAC,EACpF,IAAI;AACP,UAAI,WAAW;AACb,WAAG,OAAO,gBAAgB,EACvB,IAAI,EAAE,OAAO,CAAC,EACd,MAAM,GAAG,iBAAiB,aAAa,EAAE,CAAC,EAC1C,IAAI;AAAA,IACX,CAAC;AACD,WAAO,KAAK,IAAI,EAAE;AAAA,EACpB;AAAA,EAEA,KAAK,IAAY,OAAiB,YAAY,OAAqB;AACjE,SAAK,SAAS,YAAY,CAAC,OAAO;AAChC,YAAM,UAAU,GACb,OAAO,UAAU,EACjB,IAAI,EAAE,OAAO,YAAY,YAAY,UAAU,OAAO,WAAW,KAAK,IAAI,EAAE,CAAC,EAC7E;AAAA,QACC;AAAA,UACE,GAAG,WAAW,mBAAmB,EAAE;AAAA,UACnC,GAAG,GAAG,WAAW,OAAO,UAAU,GAAG,GAAG,WAAW,OAAO,aAAa,CAAC;AAAA,QAC1E;AAAA,MACF,EACC,IAAI;AAEP,UAAI,QAAQ,YAAY,EAAG;AAC3B,SAAG,OAAO,gBAAgB,EACvB,IAAI,YAAY,EAAE,OAAO,UAAU,IAAI,EAAE,OAAO,YAAY,SAAS,KAAK,IAAI,EAAE,CAAC,EACjF,MAAM,IAAI,GAAG,iBAAiB,aAAa,EAAE,GAAG,OAAO,iBAAiB,OAAO,CAAC,CAAC,EACjF,IAAI;AAAA,IACT,CAAC;AACD,WAAO,KAAK,IAAI,EAAE;AAAA,EACpB;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,SAAS,YAAY,CAAC,OAAO;AAChC,SAAG,OAAO,UAAU,EACjB,IAAI;AAAA,QACH,OAAO;AAAA,QACP,OAAO,EAAE,MAAM,kBAAkB,SAAS,6FAAkB;AAAA,QAC5D,WAAW,KAAK,IAAI;AAAA,MACtB,CAAC,EACA,MAAM,GAAG,WAAW,OAAO,UAAU,CAAC,EACtC,IAAI;AACP,SAAG,OAAO,UAAU,EACjB,IAAI;AAAA,QACH,OAAO;AAAA,QACP,OAAO,EAAE,MAAM,kBAAkB,SAAS,6FAAkB;AAAA,QAC5D,WAAW,KAAK,IAAI;AAAA,MACtB,CAAC,EACA,MAAM,GAAG,WAAW,OAAO,aAAa,CAAC,EACzC,IAAI;AACP,SAAG,OAAO,gBAAgB,EACvB,IAAI,EAAE,OAAO,UAAU,CAAC,EACxB,MAAM,OAAO,iBAAiB,OAAO,CAAC,EACtC,IAAI;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,gBAAmC;AACjC,WAAO,KAAK,SACT,OAAO,EACP,KAAK,gBAAgB,EACrB,MAAM,OAAO,iBAAiB,OAAO,CAAC,EACtC,IAAI;AAAA,EACT;AAAA,EAEA,uBAAuC;AACrC,WAAO,KAAK,SACT,OAAO,EACP,KAAK,UAAU,EACf;AAAA,MACC;AAAA,QACE,GAAG,WAAW,OAAO,UAAU;AAAA,QAC/B,GAAG,WAAW,OAAO,aAAa;AAAA,QAClC,IAAI,GAAG,WAAW,OAAO,SAAS,GAAG,OAAO,WAAW,YAAY,CAAC;AAAA,MACtE;AAAA,IACF,EACC,IAAI;AAAA,EACT;AAAA,EAEA,OAAO,aAAkD;AACvD,WAAO,KAAK,SACT,OAAO,EACP,KAAK,gBAAgB,EACrB,MAAM,GAAG,iBAAiB,aAAa,WAAW,CAAC,EACnD,IAAI;AAAA,EACT;AAAA,EAEA,gBAAgB,UAA+C;AAC7D,WAAO,KAAK,SACT,OAAO,EACP,KAAK,gBAAgB,EACrB,MAAM,IAAI,GAAG,iBAAiB,UAAU,QAAQ,GAAG,OAAO,iBAAiB,OAAO,CAAC,CAAC,EACpF,IAAI;AAAA,EACT;AAAA,EAEA,WAAW,aAAqB,QAAsB;AACpD,SAAK,SACF,OAAO,gBAAgB,EACvB,IAAI,EAAE,OAAO,CAAC,EACd,MAAM,IAAI,GAAG,iBAAiB,aAAa,WAAW,GAAG,OAAO,iBAAiB,OAAO,CAAC,CAAC,EAC1F,IAAI;AAAA,EACT;AAAA,EAEA,WAAW,UAAkB,QAAgB,YAA0B;AACrE,SAAK,SACF,OAAO,gBAAgB,EACvB,IAAI,EAAE,OAAO,YAAY,SAAS,KAAK,IAAI,EAAE,CAAC,EAC9C;AAAA,MACC;AAAA,QACE,GAAG,iBAAiB,UAAU,QAAQ;AAAA,QACtC,GAAG,iBAAiB,QAAQ,MAAM;AAAA,QAClC,GAAG,iBAAiB,YAAY,UAAU;AAAA,QAC1C,OAAO,iBAAiB,OAAO;AAAA,MACjC;AAAA,IACF,EACC,IAAI;AAAA,EACT;AAAA;AAAA,EAGA,UAAU,mBAA2B,2BAAyC;AAC5E,SAAK,SAAS,YAAY,CAAC,OAAO;AAChC,YAAM,MAAM,KAAK,IAAI;AACrB,SAAG,OAAO,UAAU,EACjB,IAAI,EAAE,cAAc,KAAK,wBAAwB,kBAAkB,CAAC,EACpE;AAAA,QACC,IAAI,GAAG,WAAW,mBAAmB,iBAAiB,GAAG,OAAO,WAAW,YAAY,CAAC;AAAA,MAC1F,EACC,IAAI;AACP,SAAG,OAAO,gBAAgB,EACvB,IAAI,EAAE,OAAO,cAAc,SAAS,KAAK,cAAc,KAAK,0BAA0B,CAAC,EACvF;AAAA,QACC;AAAA,UACE,GAAG,iBAAiB,aAAa,iBAAiB;AAAA,UAClD,OAAO,iBAAiB,OAAO;AAAA,QACjC;AAAA,MACF,EACC,IAAI;AACP,SAAG,OAAO,UAAU,EACjB,IAAI;AAAA,QACH,OAAO;AAAA,QACP,QAAQ,EAAE,mBAAmB,UAAU,mBAAmB,cAAc,IAAI;AAAA,QAC5E,WAAW;AAAA,MACb,CAAC,EACA;AAAA,QACC;AAAA,UACE,GAAG,WAAW,mBAAmB,yBAAyB;AAAA,UAC1D,GAAG,WAAW,OAAO,aAAa;AAAA,QACpC;AAAA,MACF,EACC,IAAI;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,MAA2C;AACpD,SAAK,SACF,OAAO,WAAW,EAClB,OAAO,EAAE,GAAG,MAAM,WAAW,KAAK,IAAI,EAAE,CAAC,EACzC,mBAAmB,EAAE,QAAQ,YAAY,UAAU,KAAK,EAAE,GAAG,MAAM,WAAW,KAAK,IAAI,EAAE,EAAE,CAAC,EAC5F,IAAI;AAAA,EACT;AAAA,EAEA,cAAc,UAA0C;AACtD,WAAO,KAAK,SAAS,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,GAAG,YAAY,UAAU,QAAQ,CAAC,EAAE,IAAI;AAAA,EAChG;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
check,
|
|
4
|
+
index,
|
|
5
|
+
integer,
|
|
6
|
+
primaryKey,
|
|
7
|
+
sqliteTable,
|
|
8
|
+
text,
|
|
9
|
+
uniqueIndex
|
|
10
|
+
} from "drizzle-orm/sqlite-core";
|
|
11
|
+
const operations = sqliteTable(
|
|
12
|
+
"operations",
|
|
13
|
+
{
|
|
14
|
+
clientOperationId: text("client_operation_id").primaryKey(),
|
|
15
|
+
kind: text("kind").notNull(),
|
|
16
|
+
target: text("target").notNull(),
|
|
17
|
+
projectId: text("project_id"),
|
|
18
|
+
requestDigest: text("request_digest").notNull(),
|
|
19
|
+
state: text("state", {
|
|
20
|
+
enum: ["accepted", "dispatching", "acknowledged", "failed", "unknown"]
|
|
21
|
+
}).notNull(),
|
|
22
|
+
result: text("result", { mode: "json" }).$type(),
|
|
23
|
+
error: text("error", { mode: "json" }).$type(),
|
|
24
|
+
createdAt: integer("created_at").notNull(),
|
|
25
|
+
updatedAt: integer("updated_at").notNull(),
|
|
26
|
+
reconciledAt: integer("reconciled_at"),
|
|
27
|
+
reconciliationDecision: text("reconciliation_decision")
|
|
28
|
+
},
|
|
29
|
+
(table) => [
|
|
30
|
+
check(
|
|
31
|
+
"operations_state_check",
|
|
32
|
+
sql`${table.state} in ('accepted', 'dispatching', 'acknowledged', 'failed', 'unknown')`
|
|
33
|
+
),
|
|
34
|
+
index("operations_state_idx").on(table.state)
|
|
35
|
+
]
|
|
36
|
+
);
|
|
37
|
+
const executionMarkers = sqliteTable(
|
|
38
|
+
"execution_markers",
|
|
39
|
+
{
|
|
40
|
+
operationId: text("operation_id").primaryKey().references(() => operations.clientOperationId),
|
|
41
|
+
threadId: text("thread_id").notNull(),
|
|
42
|
+
turnId: text("turn_id"),
|
|
43
|
+
workspaceKey: text("workspace_key").notNull(),
|
|
44
|
+
codexEpoch: text("codex_epoch").notNull(),
|
|
45
|
+
state: text("state", { enum: ["active", "unknown", "terminal", "reconciled"] }).notNull(),
|
|
46
|
+
createdAt: integer("created_at").notNull(),
|
|
47
|
+
endedAt: integer("ended_at"),
|
|
48
|
+
reconciledAt: integer("reconciled_at"),
|
|
49
|
+
reconciliationOperationId: text("reconciliation_operation_id").references(
|
|
50
|
+
() => operations.clientOperationId
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
(table) => [
|
|
54
|
+
check(
|
|
55
|
+
"execution_markers_state_check",
|
|
56
|
+
sql`${table.state} in ('active', 'unknown', 'terminal', 'reconciled')`
|
|
57
|
+
),
|
|
58
|
+
uniqueIndex("execution_markers_active_thread").on(table.threadId).where(sql`${table.endedAt} is null`)
|
|
59
|
+
]
|
|
60
|
+
);
|
|
61
|
+
const threadLinks = sqliteTable("thread_links", {
|
|
62
|
+
threadId: text("thread_id").primaryKey(),
|
|
63
|
+
projectId: text("project_id").notNull(),
|
|
64
|
+
cwd: text("cwd").notNull(),
|
|
65
|
+
source: text("source").notNull(),
|
|
66
|
+
managedEpoch: text("managed_epoch"),
|
|
67
|
+
updatedAt: integer("updated_at").notNull()
|
|
68
|
+
});
|
|
69
|
+
const preferences = sqliteTable("preferences", {
|
|
70
|
+
key: text("key").primaryKey(),
|
|
71
|
+
value: text("value", { mode: "json" }).$type().notNull()
|
|
72
|
+
});
|
|
73
|
+
const turnUsage = sqliteTable(
|
|
74
|
+
"turn_usage",
|
|
75
|
+
{
|
|
76
|
+
threadId: text("thread_id").notNull(),
|
|
77
|
+
turnId: text("turn_id").notNull(),
|
|
78
|
+
outputTokens: integer("output_tokens").notNull()
|
|
79
|
+
},
|
|
80
|
+
(table) => [primaryKey({ columns: [table.threadId, table.turnId] })]
|
|
81
|
+
);
|
|
82
|
+
const projectRegistrations = sqliteTable("project_registrations", {
|
|
83
|
+
id: text("id").primaryKey(),
|
|
84
|
+
name: text("name").notNull(),
|
|
85
|
+
path: text("path").notNull(),
|
|
86
|
+
canonicalRoot: text("canonical_root"),
|
|
87
|
+
removedAt: integer("removed_at"),
|
|
88
|
+
createdAt: integer("created_at").notNull(),
|
|
89
|
+
updatedAt: integer("updated_at").notNull()
|
|
90
|
+
});
|
|
91
|
+
export {
|
|
92
|
+
executionMarkers,
|
|
93
|
+
operations,
|
|
94
|
+
preferences,
|
|
95
|
+
projectRegistrations,
|
|
96
|
+
threadLinks,
|
|
97
|
+
turnUsage
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/server/db/schema.ts"],
|
|
4
|
+
"sourcesContent": ["import { sql } from 'drizzle-orm';\nimport {\n check,\n index,\n integer,\n primaryKey,\n sqliteTable,\n text,\n uniqueIndex,\n} from 'drizzle-orm/sqlite-core';\nimport type { ApiError } from '../../shared/types.js';\n\nexport const operations = sqliteTable(\n 'operations',\n {\n clientOperationId: text('client_operation_id').primaryKey(),\n kind: text('kind').notNull(),\n target: text('target').notNull(),\n projectId: text('project_id'),\n requestDigest: text('request_digest').notNull(),\n state: text('state', {\n enum: ['accepted', 'dispatching', 'acknowledged', 'failed', 'unknown'],\n }).notNull(),\n result: text('result', { mode: 'json' }).$type<unknown>(),\n error: text('error', { mode: 'json' }).$type<ApiError>(),\n createdAt: integer('created_at').notNull(),\n updatedAt: integer('updated_at').notNull(),\n reconciledAt: integer('reconciled_at'),\n reconciliationDecision: text('reconciliation_decision'),\n },\n (table) => [\n check(\n 'operations_state_check',\n sql`${table.state} in ('accepted', 'dispatching', 'acknowledged', 'failed', 'unknown')`,\n ),\n index('operations_state_idx').on(table.state),\n ],\n);\n\nexport const executionMarkers = sqliteTable(\n 'execution_markers',\n {\n operationId: text('operation_id')\n .primaryKey()\n .references(() => operations.clientOperationId),\n threadId: text('thread_id').notNull(),\n turnId: text('turn_id'),\n workspaceKey: text('workspace_key').notNull(),\n codexEpoch: text('codex_epoch').notNull(),\n state: text('state', { enum: ['active', 'unknown', 'terminal', 'reconciled'] }).notNull(),\n createdAt: integer('created_at').notNull(),\n endedAt: integer('ended_at'),\n reconciledAt: integer('reconciled_at'),\n reconciliationOperationId: text('reconciliation_operation_id').references(\n () => operations.clientOperationId,\n ),\n },\n (table) => [\n check(\n 'execution_markers_state_check',\n sql`${table.state} in ('active', 'unknown', 'terminal', 'reconciled')`,\n ),\n uniqueIndex('execution_markers_active_thread')\n .on(table.threadId)\n .where(sql`${table.endedAt} is null`),\n ],\n);\n\nexport const threadLinks = sqliteTable('thread_links', {\n threadId: text('thread_id').primaryKey(),\n projectId: text('project_id').notNull(),\n cwd: text('cwd').notNull(),\n source: text('source').notNull(),\n managedEpoch: text('managed_epoch'),\n updatedAt: integer('updated_at').notNull(),\n});\n\nexport const preferences = sqliteTable('preferences', {\n key: text('key').primaryKey(),\n value: text('value', { mode: 'json' }).$type<unknown>().notNull(),\n});\n\n/** Locally observed turn totals; upstream turn history does not include token usage. */\nexport const turnUsage = sqliteTable(\n 'turn_usage',\n {\n threadId: text('thread_id').notNull(),\n turnId: text('turn_id').notNull(),\n outputTokens: integer('output_tokens').notNull(),\n },\n (table) => [primaryKey({ columns: [table.threadId, table.turnId] })],\n);\n\n/** Removing a registration retains its identity and all existing thread links. */\nexport const projectRegistrations = sqliteTable('project_registrations', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n path: text('path').notNull(),\n canonicalRoot: text('canonical_root'),\n removedAt: integer('removed_at'),\n createdAt: integer('created_at').notNull(),\n updatedAt: integer('updated_at').notNull(),\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { turnUsage } from "./schema.js";
|
|
3
|
+
class TurnUsageStore {
|
|
4
|
+
constructor(database) {
|
|
5
|
+
this.database = database;
|
|
6
|
+
}
|
|
7
|
+
database;
|
|
8
|
+
save(threadId, turn) {
|
|
9
|
+
if (turn.outputTokens === void 0 || !Number.isSafeInteger(turn.outputTokens) || turn.outputTokens < 0)
|
|
10
|
+
return;
|
|
11
|
+
this.database.insert(turnUsage).values({
|
|
12
|
+
threadId,
|
|
13
|
+
turnId: turn.id,
|
|
14
|
+
outputTokens: turn.outputTokens
|
|
15
|
+
}).onConflictDoNothing().run();
|
|
16
|
+
}
|
|
17
|
+
restore(threadId, turns) {
|
|
18
|
+
if (turns.length === 0) return turns;
|
|
19
|
+
const saved = new Map(
|
|
20
|
+
this.database.select().from(turnUsage).where(eq(turnUsage.threadId, threadId)).all().map((entry) => [entry.turnId, entry.outputTokens])
|
|
21
|
+
);
|
|
22
|
+
return turns.map((turn) => {
|
|
23
|
+
const outputTokens = saved.get(turn.id);
|
|
24
|
+
return outputTokens === void 0 ? turn : { ...turn, outputTokens };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
TurnUsageStore
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=turn-usage.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/server/db/turn-usage.ts"],
|
|
4
|
+
"sourcesContent": ["import { eq } from 'drizzle-orm';\nimport type { AppDatabase } from './index.js';\nimport { turnUsage } from './schema.js';\nimport type { TurnView } from '../../shared/types.js';\n\nexport class TurnUsageStore {\n constructor(private readonly database: AppDatabase) {}\n\n save(threadId: string, turn: TurnView): void {\n if (\n turn.outputTokens === undefined ||\n !Number.isSafeInteger(turn.outputTokens) ||\n turn.outputTokens < 0\n )\n return;\n this.database\n .insert(turnUsage)\n .values({\n threadId,\n turnId: turn.id,\n outputTokens: turn.outputTokens,\n })\n .onConflictDoNothing()\n .run();\n }\n\n restore(threadId: string, turns: TurnView[]): TurnView[] {\n if (turns.length === 0) return turns;\n const saved = new Map(\n this.database\n .select()\n .from(turnUsage)\n .where(eq(turnUsage.threadId, threadId))\n .all()\n .map((entry) => [entry.turnId, entry.outputTokens]),\n );\n return turns.map((turn) => {\n const outputTokens = saved.get(turn.id);\n return outputTokens === undefined ? turn : { ...turn, outputTokens };\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,UAAU;AAEnB,SAAS,iBAAiB;AAGnB,MAAM,eAAe;AAAA,EAC1B,YAA6B,UAAuB;AAAvB;AAAA,EAAwB;AAAA,EAAxB;AAAA,EAE7B,KAAK,UAAkB,MAAsB;AAC3C,QACE,KAAK,iBAAiB,UACtB,CAAC,OAAO,cAAc,KAAK,YAAY,KACvC,KAAK,eAAe;AAEpB;AACF,SAAK,SACF,OAAO,SAAS,EAChB,OAAO;AAAA,MACN;AAAA,MACA,QAAQ,KAAK;AAAA,MACb,cAAc,KAAK;AAAA,IACrB,CAAC,EACA,oBAAoB,EACpB,IAAI;AAAA,EACT;AAAA,EAEA,QAAQ,UAAkB,OAA+B;AACvD,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,UAAM,QAAQ,IAAI;AAAA,MAChB,KAAK,SACF,OAAO,EACP,KAAK,SAAS,EACd,MAAM,GAAG,UAAU,UAAU,QAAQ,CAAC,EACtC,IAAI,EACJ,IAAI,CAAC,UAAU,CAAC,MAAM,QAAQ,MAAM,YAAY,CAAC;AAAA,IACtD;AACA,WAAO,MAAM,IAAI,CAAC,SAAS;AACzB,YAAM,eAAe,MAAM,IAAI,KAAK,EAAE;AACtC,aAAO,iBAAiB,SAAY,OAAO,EAAE,GAAG,MAAM,aAAa;AAAA,IACrE,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
class EventHub {
|
|
3
|
+
constructor(maxBytes = 16 * 1024 * 1024) {
|
|
4
|
+
this.maxBytes = maxBytes;
|
|
5
|
+
}
|
|
6
|
+
maxBytes;
|
|
7
|
+
serverEpoch = randomUUID();
|
|
8
|
+
seq = 0;
|
|
9
|
+
discardedThrough = 0;
|
|
10
|
+
bytes = 0;
|
|
11
|
+
events = [];
|
|
12
|
+
listeners = /* @__PURE__ */ new Set();
|
|
13
|
+
runtimeListeners = /* @__PURE__ */ new Set();
|
|
14
|
+
overviewListeners = /* @__PURE__ */ new Set();
|
|
15
|
+
cursor() {
|
|
16
|
+
return { serverEpoch: this.serverEpoch, seq: this.seq };
|
|
17
|
+
}
|
|
18
|
+
publish(kind, payload, threadId) {
|
|
19
|
+
const event = {
|
|
20
|
+
type: "event",
|
|
21
|
+
cursor: { serverEpoch: this.serverEpoch, seq: ++this.seq },
|
|
22
|
+
kind,
|
|
23
|
+
payload: structuredClone(payload),
|
|
24
|
+
...threadId === void 0 ? {} : { threadId }
|
|
25
|
+
};
|
|
26
|
+
const bytes = Buffer.byteLength(JSON.stringify(event));
|
|
27
|
+
this.events.push({ event, bytes });
|
|
28
|
+
this.bytes += bytes;
|
|
29
|
+
while (this.bytes > this.maxBytes && this.events.length > 0) {
|
|
30
|
+
const removed = this.events.shift();
|
|
31
|
+
this.bytes -= removed.bytes;
|
|
32
|
+
this.discardedThrough = removed.event.cursor.seq;
|
|
33
|
+
}
|
|
34
|
+
for (const listener of this.listeners)
|
|
35
|
+
if (!event.threadId || event.threadId === listener.threadId)
|
|
36
|
+
this.deliver(listener.send, event);
|
|
37
|
+
if (kind === "runtime")
|
|
38
|
+
for (const listener of this.runtimeListeners) this.deliver(listener, event);
|
|
39
|
+
}
|
|
40
|
+
subscribeRuntime(send) {
|
|
41
|
+
this.runtimeListeners.add(send);
|
|
42
|
+
return () => {
|
|
43
|
+
this.runtimeListeners.delete(send);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Overview is recovered via HTTP; it never consumes the detailed replay buffer. */
|
|
47
|
+
publishSummary(summary) {
|
|
48
|
+
const event = {
|
|
49
|
+
type: "event",
|
|
50
|
+
kind: "threadSummary",
|
|
51
|
+
threadId: summary.id,
|
|
52
|
+
cursor: this.cursor(),
|
|
53
|
+
payload: structuredClone(summary)
|
|
54
|
+
};
|
|
55
|
+
for (const listener of this.overviewListeners) this.deliver(listener, event);
|
|
56
|
+
}
|
|
57
|
+
subscribeOverview(send) {
|
|
58
|
+
this.overviewListeners.add(send);
|
|
59
|
+
return () => {
|
|
60
|
+
this.overviewListeners.delete(send);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Only connected overview clients receive this signal. It never enters the replay buffer. */
|
|
64
|
+
publishCompletion(completion) {
|
|
65
|
+
const event = {
|
|
66
|
+
type: "event",
|
|
67
|
+
kind: "turnCompletion",
|
|
68
|
+
threadId: completion.threadId,
|
|
69
|
+
cursor: this.cursor(),
|
|
70
|
+
payload: structuredClone(completion)
|
|
71
|
+
};
|
|
72
|
+
for (const listener of this.overviewListeners) this.deliver(listener, event);
|
|
73
|
+
}
|
|
74
|
+
/** Project snapshots have their own update boundary even without a thread event. */
|
|
75
|
+
publishProjects(projects) {
|
|
76
|
+
const event = {
|
|
77
|
+
type: "event",
|
|
78
|
+
kind: "projects",
|
|
79
|
+
cursor: { serverEpoch: this.serverEpoch, seq: ++this.seq },
|
|
80
|
+
payload: structuredClone(projects)
|
|
81
|
+
};
|
|
82
|
+
for (const listener of this.overviewListeners) this.deliver(listener, event);
|
|
83
|
+
}
|
|
84
|
+
subscribe(threadId, snapshot, after, send) {
|
|
85
|
+
const replayable = after?.serverEpoch === this.serverEpoch && after.seq >= this.discardedThrough && after.seq <= this.seq;
|
|
86
|
+
const queued = [];
|
|
87
|
+
let initializing = true;
|
|
88
|
+
const listener = {
|
|
89
|
+
threadId,
|
|
90
|
+
send: (message) => {
|
|
91
|
+
if (initializing) queued.push(message);
|
|
92
|
+
else send(message);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
this.listeners.add(listener);
|
|
96
|
+
let boundary = this.seq;
|
|
97
|
+
if (replayable && after) {
|
|
98
|
+
for (const { event } of [...this.events])
|
|
99
|
+
if (event.cursor.seq > after.seq && event.cursor.seq <= boundary && (!event.threadId || event.threadId === threadId))
|
|
100
|
+
this.deliver(send, event);
|
|
101
|
+
} else {
|
|
102
|
+
const data = structuredClone(snapshot());
|
|
103
|
+
boundary = this.seq;
|
|
104
|
+
this.deliver(send, { type: "snapshot", cursor: this.cursor(), threadId, data });
|
|
105
|
+
}
|
|
106
|
+
initializing = false;
|
|
107
|
+
for (const message of queued)
|
|
108
|
+
if (message.type === "event" && message.cursor.seq > boundary) this.deliver(send, message);
|
|
109
|
+
return () => {
|
|
110
|
+
this.listeners.delete(listener);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
deliver(send, message) {
|
|
114
|
+
try {
|
|
115
|
+
send(message);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
EventHub
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/server/events.ts"],
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto';\nimport type {\n Cursor,\n Operation,\n Project,\n ServerMessage,\n SystemInfo,\n ThreadSummary,\n ThreadView,\n TurnCompletion,\n} from '../shared/types.js';\n\ntype BusinessEvent = Extract<ServerMessage, { type: 'event' }>;\ntype Listener = (message: ServerMessage) => void;\n\n/** Synchronous snapshot+subscription forms one JS event-loop boundary. */\nexport class EventHub {\n readonly serverEpoch = randomUUID();\n private seq = 0;\n private discardedThrough = 0;\n private bytes = 0;\n private readonly events: { event: BusinessEvent; bytes: number }[] = [];\n private readonly listeners = new Set<{ threadId: string; send: Listener }>();\n private readonly runtimeListeners = new Set<Listener>();\n private readonly overviewListeners = new Set<Listener>();\n\n constructor(private readonly maxBytes = 16 * 1024 * 1024) {}\n\n cursor(): Cursor {\n return { serverEpoch: this.serverEpoch, seq: this.seq };\n }\n\n publish(\n kind: 'thread' | 'runtime' | 'operation',\n payload: ThreadView | SystemInfo | Operation,\n threadId?: string,\n ): void {\n const event: BusinessEvent = {\n type: 'event',\n cursor: { serverEpoch: this.serverEpoch, seq: ++this.seq },\n kind,\n payload: structuredClone(payload),\n ...(threadId === undefined ? {} : { threadId }),\n };\n const bytes = Buffer.byteLength(JSON.stringify(event));\n this.events.push({ event, bytes });\n this.bytes += bytes;\n while (this.bytes > this.maxBytes && this.events.length > 0) {\n const removed = this.events.shift()!;\n this.bytes -= removed.bytes;\n this.discardedThrough = removed.event.cursor.seq;\n }\n for (const listener of this.listeners)\n if (!event.threadId || event.threadId === listener.threadId)\n this.deliver(listener.send, event);\n if (kind === 'runtime')\n for (const listener of this.runtimeListeners) this.deliver(listener, event);\n }\n\n subscribeRuntime(send: Listener): () => void {\n this.runtimeListeners.add(send);\n return () => {\n this.runtimeListeners.delete(send);\n };\n }\n\n /** Overview is recovered via HTTP; it never consumes the detailed replay buffer. */\n publishSummary(summary: ThreadSummary): void {\n const event: BusinessEvent = {\n type: 'event',\n kind: 'threadSummary',\n threadId: summary.id,\n cursor: this.cursor(),\n payload: structuredClone(summary),\n };\n for (const listener of this.overviewListeners) this.deliver(listener, event);\n }\n\n subscribeOverview(send: Listener): () => void {\n this.overviewListeners.add(send);\n return () => {\n this.overviewListeners.delete(send);\n };\n }\n\n /** Only connected overview clients receive this signal. It never enters the replay buffer. */\n publishCompletion(completion: TurnCompletion): void {\n const event: BusinessEvent = {\n type: 'event',\n kind: 'turnCompletion',\n threadId: completion.threadId,\n cursor: this.cursor(),\n payload: structuredClone(completion),\n };\n for (const listener of this.overviewListeners) this.deliver(listener, event);\n }\n\n /** Project snapshots have their own update boundary even without a thread event. */\n publishProjects(projects: Project[]): void {\n const event: BusinessEvent = {\n type: 'event',\n kind: 'projects',\n cursor: { serverEpoch: this.serverEpoch, seq: ++this.seq },\n payload: structuredClone(projects),\n };\n for (const listener of this.overviewListeners) this.deliver(listener, event);\n }\n\n subscribe(\n threadId: string,\n snapshot: () => ThreadView,\n after: Cursor | undefined,\n send: Listener,\n ): () => void {\n const replayable =\n after?.serverEpoch === this.serverEpoch &&\n after.seq >= this.discardedThrough &&\n after.seq <= this.seq;\n const queued: ServerMessage[] = [];\n let initializing = true;\n const listener = {\n threadId,\n send: (message: ServerMessage) => {\n if (initializing) queued.push(message);\n else send(message);\n },\n };\n this.listeners.add(listener);\n // No await may occur between reading the snapshot and registering the listener.\n let boundary = this.seq;\n if (replayable && after) {\n for (const { event } of [...this.events])\n if (\n event.cursor.seq > after.seq &&\n event.cursor.seq <= boundary &&\n (!event.threadId || event.threadId === threadId)\n )\n this.deliver(send, event);\n } else {\n const data = structuredClone(snapshot());\n boundary = this.seq;\n this.deliver(send, { type: 'snapshot', cursor: this.cursor(), threadId, data });\n }\n initializing = false;\n for (const message of queued)\n if (message.type === 'event' && message.cursor.seq > boundary) this.deliver(send, message);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private deliver(send: Listener, message: ServerMessage): void {\n // A closed or slow browser must never unwind the upstream event consumer.\n try {\n send(message);\n } catch {\n /* Transport closes or unsubscribes its own failed socket. */\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { bodyLimit } from "hono/body-limit";
|
|
3
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4
|
+
import { upgradeWebSocket } from "@hono/node-server";
|
|
5
|
+
import { ZodError } from "zod";
|
|
6
|
+
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { IMAGE_REQUEST_BYTES } from "../shared/images.js";
|
|
8
|
+
import * as schemas from "../shared/validation.js";
|
|
9
|
+
import { ConfigStore } from "./config-store.js";
|
|
10
|
+
import { createAuthenticator } from "./auth.js";
|
|
11
|
+
import { socketHandlers } from "./websocket.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { releaseChannels } from "../shared/updates.js";
|
|
14
|
+
import { settingsSchema, updateRequestSchema } from "../updates/manager.js";
|
|
15
|
+
function createApp({
|
|
16
|
+
config,
|
|
17
|
+
service,
|
|
18
|
+
staticDir,
|
|
19
|
+
authenticate = createAuthenticator(config),
|
|
20
|
+
updates
|
|
21
|
+
}) {
|
|
22
|
+
const app = new Hono();
|
|
23
|
+
app.onError((error, c) => {
|
|
24
|
+
const failure = error instanceof AppError ? error : error instanceof ZodError || error instanceof SyntaxError ? new AppError("VALIDATION_FAILED", "Invalid request data", 400) : new AppError("INTERNAL_ERROR", "An internal error occurred", 500);
|
|
25
|
+
return c.json({ error: failure.toJSON(c.get("operationId")) }, failure.status);
|
|
26
|
+
});
|
|
27
|
+
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
28
|
+
app.use("*", async (c, next) => {
|
|
29
|
+
c.set("identity", await authenticate(c.req.raw, c.req.path === "/ws"));
|
|
30
|
+
await next();
|
|
31
|
+
});
|
|
32
|
+
app.use(
|
|
33
|
+
"/api/*",
|
|
34
|
+
(c, next) => bodyLimit({
|
|
35
|
+
maxSize: /^\/api\/threads\/[^/]+\/(turns|steer)$/.test(c.req.path) ? IMAGE_REQUEST_BYTES : 512 * 1024,
|
|
36
|
+
onError: () => {
|
|
37
|
+
throw new AppError("BODY_TOO_LARGE", "Request body exceeds limit", 413);
|
|
38
|
+
}
|
|
39
|
+
})(c, next)
|
|
40
|
+
);
|
|
41
|
+
app.use("/api/*", async (c, next) => {
|
|
42
|
+
c.header("Cache-Control", "no-store");
|
|
43
|
+
if (service.isInMaintenance() && !["GET", "HEAD", "OPTIONS"].includes(c.req.method) && !c.req.path.startsWith("/api/updates"))
|
|
44
|
+
throw new AppError("UPDATE_MAINTENANCE", "\u6B63\u5728\u5347\u7EA7\uFF0C\u8BF7\u7B49\u5F85\u670D\u52A1\u91CD\u65B0\u8FDE\u63A5", 503);
|
|
45
|
+
await next();
|
|
46
|
+
});
|
|
47
|
+
if (updates) {
|
|
48
|
+
app.get("/api/updates", async (c) => c.json(await updates.status()));
|
|
49
|
+
app.post("/api/updates/check", async (c) => {
|
|
50
|
+
const input = z.object({ channel: z.enum(releaseChannels).optional() }).strict().parse(await c.req.json());
|
|
51
|
+
return c.json(await updates.check(input.channel));
|
|
52
|
+
});
|
|
53
|
+
app.post(
|
|
54
|
+
"/api/updates/settings",
|
|
55
|
+
async (c) => c.json(await updates.configure(settingsSchema.parse(await c.req.json())))
|
|
56
|
+
);
|
|
57
|
+
app.post(
|
|
58
|
+
"/api/updates/run",
|
|
59
|
+
async (c) => c.json(await updates.start(updateRequestSchema.parse(await c.req.json())), 202)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const id = (value) => schemas.idSchema.parse(value);
|
|
63
|
+
const parse = async (c, schema) => {
|
|
64
|
+
const value = await c.req.json();
|
|
65
|
+
if (value && typeof value === "object" && "clientOperationId" in value && typeof value.clientOperationId === "string")
|
|
66
|
+
c.set("operationId", value.clientOperationId);
|
|
67
|
+
return schema.parse(value);
|
|
68
|
+
};
|
|
69
|
+
const status = (op) => op.status === "dispatching" || op.status === "accepted" ? 202 : 200;
|
|
70
|
+
const configuration = new ConfigStore(config);
|
|
71
|
+
app.use("/api/config*", async (c, next) => {
|
|
72
|
+
c.header("Cache-Control", "no-store");
|
|
73
|
+
await next();
|
|
74
|
+
});
|
|
75
|
+
app.get("/api/config", async (c) => c.json(await configuration.snapshot()));
|
|
76
|
+
app.post("/api/config", async (c) => c.json(await configuration.save(await c.req.json())));
|
|
77
|
+
app.post(
|
|
78
|
+
"/api/config/import",
|
|
79
|
+
async (c) => c.json(await configuration.previewImport(await c.req.json()))
|
|
80
|
+
);
|
|
81
|
+
app.get("/api/config/export", async (c) => {
|
|
82
|
+
c.header("Content-Disposition", 'attachment; filename="codex-web-config.json"');
|
|
83
|
+
return c.json(await configuration.export());
|
|
84
|
+
});
|
|
85
|
+
app.get("/api/system", (c) => c.json(service.getSystem()));
|
|
86
|
+
app.post("/api/system/compatibility/confirm", async (c) => {
|
|
87
|
+
const op = await service.confirmCompatibility(
|
|
88
|
+
await parse(c, schemas.confirmCompatibilitySchema)
|
|
89
|
+
);
|
|
90
|
+
return c.json(op, status(op));
|
|
91
|
+
});
|
|
92
|
+
app.get("/api/projects", async (c) => c.json({ data: await service.listProjects() }));
|
|
93
|
+
app.get(
|
|
94
|
+
"/api/projects/:projectId/runtime-settings",
|
|
95
|
+
async (c) => c.json(await service.readRuntimeSettings(id(c.req.param("projectId"))))
|
|
96
|
+
);
|
|
97
|
+
app.post("/api/projects/:projectId/model-defaults", async (c) => {
|
|
98
|
+
const op = await service.saveModelDefaults(
|
|
99
|
+
id(c.req.param("projectId")),
|
|
100
|
+
await parse(c, schemas.saveModelDefaultsSchema)
|
|
101
|
+
);
|
|
102
|
+
return c.json(op, status(op));
|
|
103
|
+
});
|
|
104
|
+
app.post(
|
|
105
|
+
"/api/projects",
|
|
106
|
+
async (c) => c.json(service.addProject(await parse(c, schemas.createProjectSchema)))
|
|
107
|
+
);
|
|
108
|
+
app.post(
|
|
109
|
+
"/api/projects/:projectId/rename",
|
|
110
|
+
async (c) => c.json(
|
|
111
|
+
service.renameProject(
|
|
112
|
+
id(c.req.param("projectId")),
|
|
113
|
+
await parse(c, schemas.renameProjectSchema)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
app.post("/api/projects/:projectId/remove", async (c) => {
|
|
118
|
+
await parse(c, schemas.removeProjectSchema);
|
|
119
|
+
return c.json(service.removeProject(id(c.req.param("projectId"))));
|
|
120
|
+
});
|
|
121
|
+
app.get(
|
|
122
|
+
"/api/projects/:projectId/threads",
|
|
123
|
+
async (c) => c.json(
|
|
124
|
+
await service.listThreadSummaries(
|
|
125
|
+
id(c.req.param("projectId")),
|
|
126
|
+
c.req.query("cursor") === void 0 ? void 0 : schemas.cursorSchema.parse(c.req.query("cursor")),
|
|
127
|
+
schemas.archivedQuerySchema.parse(c.req.query("archived"))
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
app.get(
|
|
132
|
+
"/api/threads",
|
|
133
|
+
async (c) => c.json(
|
|
134
|
+
await service.listThreads(
|
|
135
|
+
id(c.req.query("projectId") ?? ""),
|
|
136
|
+
c.req.query("cursor") === void 0 ? void 0 : schemas.cursorSchema.parse(c.req.query("cursor")),
|
|
137
|
+
schemas.archivedQuerySchema.parse(c.req.query("archived"))
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
app.post("/api/threads/:threadId/archive", async (c) => {
|
|
142
|
+
return c.json(
|
|
143
|
+
await service.setThreadArchived(
|
|
144
|
+
id(c.req.param("threadId")),
|
|
145
|
+
await parse(c, schemas.archiveThreadSchema)
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
app.get(
|
|
150
|
+
"/api/threads/:threadId",
|
|
151
|
+
async (c) => c.json(await service.getThread(id(c.req.param("threadId"))))
|
|
152
|
+
);
|
|
153
|
+
app.get(
|
|
154
|
+
"/api/threads/:threadId/metadata",
|
|
155
|
+
async (c) => c.json(await service.getThreadMetadata(id(c.req.param("threadId"))))
|
|
156
|
+
);
|
|
157
|
+
app.get("/api/threads/:threadId/export", async (c) => {
|
|
158
|
+
const exported = await service.exportThread(id(c.req.param("threadId")));
|
|
159
|
+
c.header("Content-Type", "text/markdown; charset=utf-8");
|
|
160
|
+
c.header("Content-Disposition", `attachment; filename="${exported.filename}"`);
|
|
161
|
+
c.header("Cache-Control", "no-store");
|
|
162
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
163
|
+
c.header("X-Export-Completeness", exported.partial ? "partial" : "saved-history");
|
|
164
|
+
return c.body(exported.markdown);
|
|
165
|
+
});
|
|
166
|
+
app.get("/api/threads/:threadId/images", async (c) => {
|
|
167
|
+
c.header("Cache-Control", "private, no-store");
|
|
168
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
169
|
+
c.header("Cross-Origin-Resource-Policy", "same-origin");
|
|
170
|
+
c.header("Content-Security-Policy", "default-src 'none'; sandbox");
|
|
171
|
+
const image = await service.getThreadImage(id(c.req.param("threadId")), c.req.query("path"));
|
|
172
|
+
return c.body(image.bytes, 200, {
|
|
173
|
+
"Content-Type": image.contentType,
|
|
174
|
+
"Content-Length": String(image.bytes.length)
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
app.get("/api/threads/:threadId/markdown", async (c) => {
|
|
178
|
+
c.header("Cache-Control", "private, no-store");
|
|
179
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
180
|
+
c.header("Cross-Origin-Resource-Policy", "same-origin");
|
|
181
|
+
return c.json(
|
|
182
|
+
await service.getThreadMarkdown(id(c.req.param("threadId")), c.req.query("path"))
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
app.get(
|
|
186
|
+
"/api/operations/:operationId",
|
|
187
|
+
(c) => c.json(service.getOperation(id(c.req.param("operationId"))))
|
|
188
|
+
);
|
|
189
|
+
app.post("/api/projects/:projectId/threads", async (c) => {
|
|
190
|
+
const op = await service.createThread(
|
|
191
|
+
id(c.req.param("projectId")),
|
|
192
|
+
await parse(c, schemas.createThreadSchema)
|
|
193
|
+
);
|
|
194
|
+
return c.json(op, status(op));
|
|
195
|
+
});
|
|
196
|
+
app.post("/api/threads/:threadId/resume", async (c) => {
|
|
197
|
+
const op = await service.resumeThread(
|
|
198
|
+
id(c.req.param("threadId")),
|
|
199
|
+
await parse(c, schemas.resumeThreadSchema)
|
|
200
|
+
);
|
|
201
|
+
return c.json(op, status(op));
|
|
202
|
+
});
|
|
203
|
+
app.post("/api/threads/:threadId/fork", async (c) => {
|
|
204
|
+
const op = await service.forkThread(
|
|
205
|
+
id(c.req.param("threadId")),
|
|
206
|
+
await parse(c, schemas.forkThreadSchema)
|
|
207
|
+
);
|
|
208
|
+
return c.json(op, status(op));
|
|
209
|
+
});
|
|
210
|
+
app.post("/api/threads/:threadId/rename", async (c) => {
|
|
211
|
+
const op = await service.renameThread(
|
|
212
|
+
id(c.req.param("threadId")),
|
|
213
|
+
await parse(c, schemas.renameThreadSchema)
|
|
214
|
+
);
|
|
215
|
+
return c.json(op, status(op));
|
|
216
|
+
});
|
|
217
|
+
app.post("/api/threads/:threadId/turns", async (c) => {
|
|
218
|
+
const op = await service.startTurn(
|
|
219
|
+
id(c.req.param("threadId")),
|
|
220
|
+
await parse(c, schemas.startTurnSchema)
|
|
221
|
+
);
|
|
222
|
+
return c.json(op, status(op));
|
|
223
|
+
});
|
|
224
|
+
app.post("/api/threads/:threadId/review", async (c) => {
|
|
225
|
+
const op = await service.startReview(
|
|
226
|
+
id(c.req.param("threadId")),
|
|
227
|
+
await parse(c, schemas.startReviewSchema)
|
|
228
|
+
);
|
|
229
|
+
return c.json(op, status(op));
|
|
230
|
+
});
|
|
231
|
+
app.post("/api/threads/:threadId/steer", async (c) => {
|
|
232
|
+
const op = await service.steer(
|
|
233
|
+
id(c.req.param("threadId")),
|
|
234
|
+
await parse(c, schemas.steerSchema)
|
|
235
|
+
);
|
|
236
|
+
return c.json(op, status(op));
|
|
237
|
+
});
|
|
238
|
+
app.post("/api/threads/:threadId/compact", async (c) => {
|
|
239
|
+
const op = await service.compactThread(
|
|
240
|
+
id(c.req.param("threadId")),
|
|
241
|
+
await parse(c, schemas.operationSchema)
|
|
242
|
+
);
|
|
243
|
+
return c.json(op, status(op));
|
|
244
|
+
});
|
|
245
|
+
app.post("/api/threads/:threadId/interrupt", async (c) => {
|
|
246
|
+
const op = await service.interrupt(
|
|
247
|
+
id(c.req.param("threadId")),
|
|
248
|
+
await parse(c, schemas.interruptSchema)
|
|
249
|
+
);
|
|
250
|
+
return c.json(op, status(op));
|
|
251
|
+
});
|
|
252
|
+
app.post("/api/actions/:actionId/resolve", async (c) => {
|
|
253
|
+
const op = await service.resolveAction(
|
|
254
|
+
id(c.req.param("actionId")),
|
|
255
|
+
await parse(c, schemas.resolveActionSchema)
|
|
256
|
+
);
|
|
257
|
+
return c.json(op, status(op));
|
|
258
|
+
});
|
|
259
|
+
app.post("/api/operations/:operationId/reconcile", async (c) => {
|
|
260
|
+
const op = await service.reconcileOperation(
|
|
261
|
+
id(c.req.param("operationId")),
|
|
262
|
+
await parse(c, schemas.reconcileSchema)
|
|
263
|
+
);
|
|
264
|
+
return c.json(op, status(op));
|
|
265
|
+
});
|
|
266
|
+
app.get(
|
|
267
|
+
"/ws",
|
|
268
|
+
upgradeWebSocket(
|
|
269
|
+
(c) => socketHandlers(service, config, c.get("identity"), c.req.query("overview") === "1")
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
app.all("/api/*", () => {
|
|
273
|
+
throw new AppError("NOT_FOUND", "API route not found", 404);
|
|
274
|
+
});
|
|
275
|
+
app.all("/api", () => {
|
|
276
|
+
throw new AppError("NOT_FOUND", "API route not found", 404);
|
|
277
|
+
});
|
|
278
|
+
app.all("/ws/*", () => {
|
|
279
|
+
throw new AppError("NOT_FOUND", "WebSocket route not found", 404);
|
|
280
|
+
});
|
|
281
|
+
app.use("*", async (c, next) => {
|
|
282
|
+
if (!c.req.path.startsWith("/assets/")) c.header("Cache-Control", "no-store");
|
|
283
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
284
|
+
c.header("Referrer-Policy", "same-origin");
|
|
285
|
+
c.header(
|
|
286
|
+
"Content-Security-Policy",
|
|
287
|
+
// Remote Markdown images load directly in the browser, including HTTPS redirects.
|
|
288
|
+
// Keep scripts, connections and all other resource policies restricted as before.
|
|
289
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'"
|
|
290
|
+
);
|
|
291
|
+
await next();
|
|
292
|
+
});
|
|
293
|
+
app.get("*", serveStatic({ root: staticDir }));
|
|
294
|
+
app.get("*", async (c, next) => {
|
|
295
|
+
if (!c.req.header("accept")?.includes("text/html") || /\.[^/]+$/.test(c.req.path))
|
|
296
|
+
return c.json({ error: { code: "NOT_FOUND", message: "Resource not found" } }, 404);
|
|
297
|
+
return serveStatic({ path: `${staticDir}/index.html` })(c, next);
|
|
298
|
+
});
|
|
299
|
+
app.notFound((c) => c.json({ error: { code: "NOT_FOUND", message: "Resource not found" } }, 404));
|
|
300
|
+
return app;
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
createApp
|
|
304
|
+
};
|
|
305
|
+
//# sourceMappingURL=http.js.map
|