| author | |
| committer | |
| log | 8efbb31725f8500e1aff76655b04a3fab7b064fe |
| tree | 9f7853eaf32211f7576ba49ca511caa208927bfe |
| parent | b4c74723d1a3072551e38d1bd3b5098e3872a51d |
4 files changed, 155 insertions(+), 151 deletions(-)
apps/server/src/git/Layers/GitCore.ts+67-54| ... | @@ -1,11 +1,19 @@ | ... | @@ -1,11 +1,19 @@ |
| 1 | import { Effect, FileSystem, Layer, Path } from "effect"; | 1 | import { Cache, Data, Duration, Effect, Exit, FileSystem, Layer, Path } from "effect"; |
| 2 | 2 | ||
| 3 | import { GitCommandError } from "../Errors.ts"; | 3 | import { GitCommandError } from "../Errors.ts"; |
| 4 | import { GitService } from "../Services/GitService.ts"; | 4 | import { GitService } from "../Services/GitService.ts"; |
| 5 | import { GitCore, type GitCoreShape } from "../Services/GitCore.ts"; | 5 | import { GitCore, type GitCoreShape } from "../Services/GitCore.ts"; |
| 6 | 6 | ||
| 7 | const STATUS_UPSTREAM_REFRESH_INTERVAL_MS = 15_000; | 7 | const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15); |
| 8 | const STATUS_UPSTREAM_REFRESH_TIMEOUT_MS = 5_000; | 8 | const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5); |
| 9 | const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048; | ||
| 10 | |||
| 11 | class StatusUpstreamRefreshCacheKey extends Data.Class<{ | ||
| 12 | cwd: string; | ||
| 13 | upstreamRef: string; | ||
| 14 | remoteName: string; | ||
| 15 | upstreamBranch: string; | ||
| 16 | }> {} | ||
| 9 | 17 | ||
| 10 | interface ExecuteGitOptions { | 18 | interface ExecuteGitOptions { |
| 11 | timeoutMs?: number | undefined; | 19 | timeoutMs?: number | undefined; |
| ... | @@ -13,10 +21,6 @@ interface ExecuteGitOptions { | ... | @@ -13,10 +21,6 @@ interface ExecuteGitOptions { |
| 13 | fallbackErrorMessage?: string | undefined; | 21 | fallbackErrorMessage?: string | undefined; |
| 14 | } | 22 | } |
| 15 | 23 | ||
| 16 | function trimStdout(value: string): string { | ||
| 17 | return value.trim(); | ||
| 18 | } | ||
| 19 | |||
| 20 | function parseBranchAb(value: string): { ahead: number; behind: number } { | 24 | function parseBranchAb(value: string): { ahead: number; behind: number } { |
| 21 | const match = value.match(/^\+(\d+)\s+-(\d+)$/); | 25 | const match = value.match(/^\+(\d+)\s+-(\d+)$/); |
| 22 | if (!match) return { ahead: 0, behind: 0 }; | 26 | if (!match) return { ahead: 0, behind: 0 }; |
| ... | @@ -96,7 +100,6 @@ const makeGitCore = Effect.gen(function* () { | ... | @@ -96,7 +100,6 @@ const makeGitCore = Effect.gen(function* () { |
| 96 | const git = yield* GitService; | 100 | const git = yield* GitService; |
| 97 | const fileSystem = yield* FileSystem.FileSystem; | 101 | const fileSystem = yield* FileSystem.FileSystem; |
| 98 | const path = yield* Path.Path; | 102 | const path = yield* Path.Path; |
| 99 | const lastUpstreamRefreshAt = new Map<string, number>(); | ||
| 100 | 103 | ||
| 101 | const executeGit = ( | 104 | const executeGit = ( |
| 102 | operation: string, | 105 | operation: string, |
| ... | @@ -162,14 +165,13 @@ const makeGitCore = Effect.gen(function* () { | ... | @@ -162,14 +165,13 @@ const makeGitCore = Effect.gen(function* () { |
| 162 | GitCommandError | 165 | GitCommandError |
| 163 | > => | 166 | > => |
| 164 | Effect.gen(function* () { | 167 | Effect.gen(function* () { |
| 165 | const upstreamRef = trimStdout( | 168 | const upstreamRef = yield* runGitStdout( |
| 166 | yield* runGitStdout( | 169 | "GitCore.resolveCurrentUpstream", |
| 167 | "GitCore.resolveCurrentUpstream", | 170 | cwd, |
| 168 | cwd, | 171 | ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], |
| 169 | ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], | 172 | true, |
| 170 | true, | 173 | ).pipe(Effect.map((stdout) => stdout.trim())); |
| 171 | ), | 174 | |
| 172 | ); | ||
| 173 | if (upstreamRef.length === 0 || upstreamRef === "@{upstream}") { | 175 | if (upstreamRef.length === 0 || upstreamRef === "@{upstream}") { |
| 174 | return null; | 176 | return null; |
| 175 | } | 177 | } |
| ... | @@ -215,36 +217,45 @@ const makeGitCore = Effect.gen(function* () { | ... | @@ -215,36 +217,45 @@ const makeGitCore = Effect.gen(function* () { |
| 215 | ["fetch", "--quiet", "--no-tags", upstream.remoteName, refspec], | 217 | ["fetch", "--quiet", "--no-tags", upstream.remoteName, refspec], |
| 216 | { | 218 | { |
| 217 | allowNonZeroExit: true, | 219 | allowNonZeroExit: true, |
| 218 | timeoutMs: STATUS_UPSTREAM_REFRESH_TIMEOUT_MS, | 220 | timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT), |
| 219 | }, | 221 | }, |
| 220 | ).pipe(Effect.asVoid); | 222 | ).pipe(Effect.asVoid); |
| 221 | }; | 223 | }; |
| 222 | 224 | ||
| 225 | const statusUpstreamRefreshCache = yield* Cache.makeWith({ | ||
| 226 | capacity: STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY, | ||
| 227 | lookup: (cacheKey: StatusUpstreamRefreshCacheKey) => | ||
| 228 | Effect.gen(function* () { | ||
| 229 | yield* fetchUpstreamRefForStatus(cacheKey.cwd, { | ||
| 230 | upstreamRef: cacheKey.upstreamRef, | ||
| 231 | remoteName: cacheKey.remoteName, | ||
| 232 | upstreamBranch: cacheKey.upstreamBranch, | ||
| 233 | }); | ||
| 234 | return true as const; | ||
| 235 | }), | ||
| 236 | // Keep successful refreshes warm; drop failures immediately so next request can retry. | ||
| 237 | timeToLive: (exit) => (Exit.isSuccess(exit) ? STATUS_UPSTREAM_REFRESH_INTERVAL : Duration.zero), | ||
| 238 | }); | ||
| 239 | |||
| 223 | const refreshStatusUpstreamIfStale = (cwd: string): Effect.Effect<void, GitCommandError> => | 240 | const refreshStatusUpstreamIfStale = (cwd: string): Effect.Effect<void, GitCommandError> => |
| 224 | Effect.gen(function* () { | 241 | Effect.gen(function* () { |
| 225 | const upstream = yield* resolveCurrentUpstream(cwd); | 242 | const upstream = yield* resolveCurrentUpstream(cwd); |
| 226 | if (!upstream) { | 243 | if (!upstream) return; |
| 227 | return; | 244 | yield* Cache.get( |
| 228 | } | 245 | statusUpstreamRefreshCache, |
| 229 | 246 | new StatusUpstreamRefreshCacheKey({ | |
| 230 | const cacheKey = `${cwd}\u0000${upstream.upstreamRef}`; | 247 | cwd, |
| 231 | const now = Date.now(); | 248 | upstreamRef: upstream.upstreamRef, |
| 232 | const lastRefreshAt = lastUpstreamRefreshAt.get(cacheKey) ?? 0; | 249 | remoteName: upstream.remoteName, |
| 233 | if (now - lastRefreshAt < STATUS_UPSTREAM_REFRESH_INTERVAL_MS) { | 250 | upstreamBranch: upstream.upstreamBranch, |
| 234 | return; | 251 | }), |
| 235 | } | 252 | ); |
| 236 | |||
| 237 | // Record attempt time first to avoid repeated fetch spikes when status is polled often. | ||
| 238 | lastUpstreamRefreshAt.set(cacheKey, now); | ||
| 239 | yield* fetchUpstreamRefForStatus(cwd, upstream); | ||
| 240 | }); | 253 | }); |
| 241 | 254 | ||
| 242 | const refreshCheckedOutBranchUpstream = (cwd: string): Effect.Effect<void, GitCommandError> => | 255 | const refreshCheckedOutBranchUpstream = (cwd: string): Effect.Effect<void, GitCommandError> => |
| 243 | Effect.gen(function* () { | 256 | Effect.gen(function* () { |
| 244 | const upstream = yield* resolveCurrentUpstream(cwd); | 257 | const upstream = yield* resolveCurrentUpstream(cwd); |
| 245 | if (!upstream) { | 258 | if (!upstream) return; |
| 246 | return; | ||
| 247 | } | ||
| 248 | yield* fetchUpstreamRef(cwd, upstream); | 259 | yield* fetchUpstreamRef(cwd, upstream); |
| 249 | }); | 260 | }); |
| 250 | 261 | ||
| ... | @@ -394,8 +405,8 @@ const makeGitCore = Effect.gen(function* () { | ... | @@ -394,8 +405,8 @@ const makeGitCore = Effect.gen(function* () { |
| 394 | "diff", | 405 | "diff", |
| 395 | "--cached", | 406 | "--cached", |
| 396 | "--name-status", | 407 | "--name-status", |
| 397 | ]); | 408 | ]).pipe(Effect.map((stdout) => stdout.trim())); |
| 398 | if (trimStdout(stagedSummary).length === 0) { | 409 | if (stagedSummary.length === 0) { |
| 399 | return null; | 410 | return null; |
| 400 | } | 411 | } |
| 401 | 412 | ||
| ... | @@ -420,9 +431,11 @@ const makeGitCore = Effect.gen(function* () { | ... | @@ -420,9 +431,11 @@ const makeGitCore = Effect.gen(function* () { |
| 420 | args.push("-m", trimmedBody); | 431 | args.push("-m", trimmedBody); |
| 421 | } | 432 | } |
| 422 | yield* runGit("GitCore.commit.commit", cwd, args); | 433 | yield* runGit("GitCore.commit.commit", cwd, args); |
| 423 | const commitSha = trimStdout( | 434 | const commitSha = yield* runGitStdout("GitCore.commit.revParseHead", cwd, [ |
| 424 | yield* runGitStdout("GitCore.commit.revParseHead", cwd, ["rev-parse", "HEAD"]), | 435 | "rev-parse", |
| 425 | ); | 436 | "HEAD", |
| 437 | ]).pipe(Effect.map((stdout) => stdout.trim())); | ||
| 438 | |||
| 426 | return { commitSha }; | 439 | return { commitSha }; |
| 427 | }); | 440 | }); |
| 428 | 441 | ||
| ... | @@ -491,21 +504,23 @@ const makeGitCore = Effect.gen(function* () { | ... | @@ -491,21 +504,23 @@ const makeGitCore = Effect.gen(function* () { |
| 491 | "Current branch has no upstream configured. Push with upstream first.", | 504 | "Current branch has no upstream configured. Push with upstream first.", |
| 492 | ); | 505 | ); |
| 493 | } | 506 | } |
| 494 | const beforeSha = trimStdout( | 507 | const beforeSha = yield* runGitStdout( |
| 495 | yield* runGitStdout( | 508 | "GitCore.pullCurrentBranch.beforeSha", |
| 496 | "GitCore.pullCurrentBranch.beforeSha", | 509 | cwd, |
| 497 | cwd, | 510 | ["rev-parse", "HEAD"], |
| 498 | ["rev-parse", "HEAD"], | 511 | true, |
| 499 | true, | 512 | ).pipe(Effect.map((stdout) => stdout.trim())); |
| 500 | ), | ||
| 501 | ); | ||
| 502 | yield* executeGit("GitCore.pullCurrentBranch.pull", cwd, ["pull", "--ff-only"], { | 513 | yield* executeGit("GitCore.pullCurrentBranch.pull", cwd, ["pull", "--ff-only"], { |
| 503 | timeoutMs: 30_000, | 514 | timeoutMs: 30_000, |
| 504 | fallbackErrorMessage: "git pull failed", | 515 | fallbackErrorMessage: "git pull failed", |
| 505 | }); | 516 | }); |
| 506 | const afterSha = trimStdout( | 517 | const afterSha = yield* runGitStdout( |
| 507 | yield* runGitStdout("GitCore.pullCurrentBranch.afterSha", cwd, ["rev-parse", "HEAD"], true), | 518 | "GitCore.pullCurrentBranch.afterSha", |
| 508 | ); | 519 | cwd, |
| 520 | ["rev-parse", "HEAD"], | ||
| 521 | true, | ||
| 522 | ).pipe(Effect.map((stdout) => stdout.trim())); | ||
| 523 | |||
| 509 | const refreshed = yield* statusDetails(cwd); | 524 | const refreshed = yield* statusDetails(cwd); |
| 510 | return { | 525 | return { |
| 511 | status: beforeSha.length > 0 && beforeSha === afterSha ? "skipped_up_to_date" : "pulled", | 526 | status: beforeSha.length > 0 && beforeSha === afterSha ? "skipped_up_to_date" : "pulled", |
| ... | @@ -540,10 +555,8 @@ const makeGitCore = Effect.gen(function* () { | ... | @@ -540,10 +555,8 @@ const makeGitCore = Effect.gen(function* () { |
| 540 | 555 | ||
| 541 | const readConfigValue: GitCoreShape["readConfigValue"] = (cwd, key) => | 556 | const readConfigValue: GitCoreShape["readConfigValue"] = (cwd, key) => |
| 542 | runGitStdout("GitCore.readConfigValue", cwd, ["config", "--get", key], true).pipe( | 557 | runGitStdout("GitCore.readConfigValue", cwd, ["config", "--get", key], true).pipe( |
| 543 | Effect.map((stdout) => { | 558 | Effect.map((stdout) => stdout.trim()), |
| 544 | const value = trimStdout(stdout); | 559 | Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)), |
| 545 | return value.length > 0 ? value : null; | ||
| 546 | }), | ||
| 547 | ); | 560 | ); |
| 548 | 561 | ||
| 549 | const listBranches: GitCoreShape["listBranches"] = (input) => | 562 | const listBranches: GitCoreShape["listBranches"] = (input) => |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+15-38| ... | @@ -8,7 +8,7 @@ import { | ... | @@ -8,7 +8,7 @@ import { |
| 8 | type ThreadId, | 8 | type ThreadId, |
| 9 | type TurnId, | 9 | type TurnId, |
| 10 | } from "@t3tools/contracts"; | 10 | } from "@t3tools/contracts"; |
| 11 | import { Effect, Layer, Queue, Stream } from "effect"; | 11 | import { Cache, Duration, Effect, Layer, Option, Queue, Stream } from "effect"; |
| 12 | 12 | ||
| 13 | import { ProviderService } from "../../provider/Services/ProviderService.ts"; | 13 | import { ProviderService } from "../../provider/Services/ProviderService.ts"; |
| 14 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; | 14 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; |
| ... | @@ -76,46 +76,23 @@ const serverCommandId = (tag: string): CommandId => | ... | @@ -76,46 +76,23 @@ const serverCommandId = (tag: string): CommandId => |
| 76 | CommandId.makeUnsafe(`server:${tag}:${crypto.randomUUID()}`); | 76 | CommandId.makeUnsafe(`server:${tag}:${crypto.randomUUID()}`); |
| 77 | 77 | ||
| 78 | const HANDLED_TURN_START_KEY_MAX = 10_000; | 78 | const HANDLED_TURN_START_KEY_MAX = 10_000; |
| 79 | const HANDLED_TURN_START_KEY_TTL_MS = 30 * 60 * 1_000; | 79 | const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); |
| 80 | |||
| 81 | function pruneHandledTurnStartKeys(handledKeys: Map<string, number>, nowMs: number): void { | ||
| 82 | const staleBefore = nowMs - HANDLED_TURN_START_KEY_TTL_MS; | ||
| 83 | for (const [key, seenAt] of handledKeys) { | ||
| 84 | if (seenAt >= staleBefore) { | ||
| 85 | break; | ||
| 86 | } | ||
| 87 | handledKeys.delete(key); | ||
| 88 | } | ||
| 89 | |||
| 90 | while (handledKeys.size > HANDLED_TURN_START_KEY_MAX) { | ||
| 91 | const oldestKey = handledKeys.keys().next().value; | ||
| 92 | if (!oldestKey) { | ||
| 93 | break; | ||
| 94 | } | ||
| 95 | handledKeys.delete(oldestKey); | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | function hasHandledTurnStartRecently( | ||
| 100 | handledKeys: Map<string, number>, | ||
| 101 | key: string, | ||
| 102 | nowMs = Date.now(), | ||
| 103 | ): boolean { | ||
| 104 | const seenAt = handledKeys.get(key); | ||
| 105 | if (seenAt !== undefined && nowMs - seenAt <= HANDLED_TURN_START_KEY_TTL_MS) { | ||
| 106 | handledKeys.set(key, nowMs); | ||
| 107 | return true; | ||
| 108 | } | ||
| 109 | |||
| 110 | handledKeys.set(key, nowMs); | ||
| 111 | pruneHandledTurnStartKeys(handledKeys, nowMs); | ||
| 112 | return false; | ||
| 113 | } | ||
| 114 | 80 | ||
| 115 | const make = Effect.gen(function* () { | 81 | const make = Effect.gen(function* () { |
| 116 | const orchestrationEngine = yield* OrchestrationEngineService; | 82 | const orchestrationEngine = yield* OrchestrationEngineService; |
| 117 | const providerService = yield* ProviderService; | 83 | const providerService = yield* ProviderService; |
| 118 | const handledTurnStartKeys = new Map<string, number>(); | 84 | const handledTurnStartKeys = yield* Cache.make<string, true>({ |
| 85 | capacity: HANDLED_TURN_START_KEY_MAX, | ||
| 86 | timeToLive: HANDLED_TURN_START_KEY_TTL, | ||
| 87 | lookup: () => Effect.succeed(true), | ||
| 88 | }); | ||
| 89 | |||
| 90 | const hasHandledTurnStartRecently = (key: string) => | ||
| 91 | Cache.getOption(handledTurnStartKeys, key).pipe( | ||
| 92 | Effect.flatMap((cached) => | ||
| 93 | Cache.set(handledTurnStartKeys, key, true).pipe(Effect.as(Option.isSome(cached))), | ||
| 94 | ), | ||
| 95 | ); | ||
| 119 | 96 | ||
| 120 | const appendProviderFailureActivity = (input: { | 97 | const appendProviderFailureActivity = (input: { |
| 121 | readonly threadId: ThreadId; | 98 | readonly threadId: ThreadId; |
| ... | @@ -246,7 +223,7 @@ const make = Effect.gen(function* () { | ... | @@ -246,7 +223,7 @@ const make = Effect.gen(function* () { |
| 246 | event: Extract<ProviderIntentEvent, { type: "thread.turn-start-requested" }>, | 223 | event: Extract<ProviderIntentEvent, { type: "thread.turn-start-requested" }>, |
| 247 | ) { | 224 | ) { |
| 248 | const key = turnStartKeyForEvent(event); | 225 | const key = turnStartKeyForEvent(event); |
| 249 | if (hasHandledTurnStartRecently(handledTurnStartKeys, key)) { | 226 | if (yield* hasHandledTurnStartRecently(key)) { |
| 250 | return; | 227 | return; |
| 251 | } | 228 | } |
| 252 | 229 |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+66-42| ... | @@ -7,7 +7,7 @@ import { | ... | @@ -7,7 +7,7 @@ import { |
| 7 | type ProviderRuntimeEvent, | 7 | type ProviderRuntimeEvent, |
| 8 | type ProviderSessionId, | 8 | type ProviderSessionId, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import { Cause, Effect, Layer, Queue, Stream } from "effect"; | 10 | import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect"; |
| 11 | 11 | ||
| 12 | import { ProviderService } from "../../provider/Services/ProviderService.ts"; | 12 | import { ProviderService } from "../../provider/Services/ProviderService.ts"; |
| 13 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; | 13 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; |
| ... | @@ -19,6 +19,8 @@ import { | ... | @@ -19,6 +19,8 @@ import { |
| 19 | const providerTurnKey = (sessionId: ProviderSessionId, turnId: TurnId) => `${sessionId}:${turnId}`; | 19 | const providerTurnKey = (sessionId: ProviderSessionId, turnId: TurnId) => `${sessionId}:${turnId}`; |
| 20 | const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId => | 20 | const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId => |
| 21 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); | 21 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); |
| 22 | const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; | ||
| 23 | const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); | ||
| 22 | 24 | ||
| 23 | function toTurnId(value: string | undefined): TurnId | undefined { | 25 | function toTurnId(value: string | undefined): TurnId | undefined { |
| 24 | return value === undefined ? undefined : TurnId.makeUnsafe(value); | 26 | return value === undefined ? undefined : TurnId.makeUnsafe(value); |
| ... | @@ -128,54 +130,76 @@ const make = Effect.gen(function* () { | ... | @@ -128,54 +130,76 @@ const make = Effect.gen(function* () { |
| 128 | const orchestrationEngine = yield* OrchestrationEngineService; | 130 | const orchestrationEngine = yield* OrchestrationEngineService; |
| 129 | const providerService = yield* ProviderService; | 131 | const providerService = yield* ProviderService; |
| 130 | 132 | ||
| 131 | const turnMessageIdsByTurnKey = new Map<string, Set<MessageId>>(); | 133 | const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({ |
| 134 | capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY, | ||
| 135 | timeToLive: TURN_MESSAGE_IDS_BY_TURN_TTL, | ||
| 136 | lookup: () => Effect.succeed(new Set<MessageId>()), | ||
| 137 | }); | ||
| 132 | 138 | ||
| 133 | const rememberAssistantMessageId = ( | 139 | const rememberAssistantMessageId = ( |
| 134 | sessionId: ProviderSessionId, | 140 | sessionId: ProviderSessionId, |
| 135 | turnId: TurnId, | 141 | turnId: TurnId, |
| 136 | messageId: MessageId, | 142 | messageId: MessageId, |
| 137 | ) => { | 143 | ) => |
| 138 | const key = providerTurnKey(sessionId, turnId); | 144 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)).pipe( |
| 139 | const existingIds = turnMessageIdsByTurnKey.get(key); | 145 | Effect.flatMap((existingIds) => |
| 140 | if (existingIds) { | 146 | Cache.set( |
| 141 | existingIds.add(messageId); | 147 | turnMessageIdsByTurnKey, |
| 142 | } else { | 148 | providerTurnKey(sessionId, turnId), |
| 143 | turnMessageIdsByTurnKey.set(key, new Set([messageId])); | 149 | Option.match(existingIds, { |
| 144 | } | 150 | onNone: () => new Set([messageId]), |
| 145 | }; | 151 | onSome: (ids) => { |
| 152 | const nextIds = new Set(ids); | ||
| 153 | nextIds.add(messageId); | ||
| 154 | return nextIds; | ||
| 155 | }, | ||
| 156 | }), | ||
| 157 | ), | ||
| 158 | ), | ||
| 159 | ); | ||
| 146 | 160 | ||
| 147 | const forgetAssistantMessageId = ( | 161 | const forgetAssistantMessageId = ( |
| 148 | sessionId: ProviderSessionId, | 162 | sessionId: ProviderSessionId, |
| 149 | turnId: TurnId, | 163 | turnId: TurnId, |
| 150 | messageId: MessageId, | 164 | messageId: MessageId, |
| 151 | ) => { | 165 | ) => |
| 152 | const key = providerTurnKey(sessionId, turnId); | 166 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)).pipe( |
| 153 | const existingIds = turnMessageIdsByTurnKey.get(key); | 167 | Effect.flatMap((existingIds) => |
| 154 | if (!existingIds) { | 168 | Option.match(existingIds, { |
| 155 | return; | 169 | onNone: () => Effect.void, |
| 156 | } | 170 | onSome: (ids) => { |
| 157 | existingIds.delete(messageId); | 171 | const nextIds = new Set(ids); |
| 158 | if (existingIds.size === 0) { | 172 | nextIds.delete(messageId); |
| 159 | turnMessageIdsByTurnKey.delete(key); | 173 | if (nextIds.size === 0) { |
| 160 | } | 174 | return Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)); |
| 161 | }; | 175 | } |
| 176 | return Cache.set(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId), nextIds); | ||
| 177 | }, | ||
| 178 | }), | ||
| 179 | ), | ||
| 180 | ); | ||
| 162 | 181 | ||
| 163 | const getAssistantMessageIdsForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => { | 182 | const getAssistantMessageIdsForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => |
| 164 | return turnMessageIdsByTurnKey.get(providerTurnKey(sessionId, turnId)) ?? new Set<MessageId>(); | 183 | Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)).pipe( |
| 165 | }; | 184 | Effect.map((existingIds) => |
| 185 | Option.getOrElse(existingIds, (): Set<MessageId> => new Set<MessageId>()), | ||
| 186 | ), | ||
| 187 | ); | ||
| 166 | 188 | ||
| 167 | const clearAssistantMessageIdsForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => { | 189 | const clearAssistantMessageIdsForTurn = (sessionId: ProviderSessionId, turnId: TurnId) => |
| 168 | turnMessageIdsByTurnKey.delete(providerTurnKey(sessionId, turnId)); | 190 | Cache.invalidate(turnMessageIdsByTurnKey, providerTurnKey(sessionId, turnId)); |
| 169 | }; | ||
| 170 | 191 | ||
| 171 | const clearTurnStateForSession = (sessionId: ProviderSessionId) => { | 192 | const clearTurnStateForSession = (sessionId: ProviderSessionId) => |
| 172 | const prefix = `${sessionId}:`; | 193 | Effect.gen(function* () { |
| 173 | for (const key of turnMessageIdsByTurnKey.keys()) { | 194 | const prefix = `${sessionId}:`; |
| 174 | if (key.startsWith(prefix)) { | 195 | const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey)); |
| 175 | turnMessageIdsByTurnKey.delete(key); | 196 | yield* Effect.forEach( |
| 176 | } | 197 | turnKeys, |
| 177 | } | 198 | (key) => |
| 178 | }; | 199 | key.startsWith(prefix) ? Cache.invalidate(turnMessageIdsByTurnKey, key) : Effect.void, |
| 200 | { concurrency: 1 }, | ||
| 201 | ).pipe(Effect.asVoid); | ||
| 202 | }); | ||
| 179 | 203 | ||
| 180 | const processEvent = (event: ProviderRuntimeEvent) => | 204 | const processEvent = (event: ProviderRuntimeEvent) => |
| 181 | Effect.gen(function* () { | 205 | Effect.gen(function* () { |
| ... | @@ -243,7 +267,7 @@ const make = Effect.gen(function* () { | ... | @@ -243,7 +267,7 @@ const make = Effect.gen(function* () { |
| 243 | ); | 267 | ); |
| 244 | const turnId = toTurnId(event.turnId); | 268 | const turnId = toTurnId(event.turnId); |
| 245 | if (turnId) { | 269 | if (turnId) { |
| 246 | rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); | 270 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); |
| 247 | } | 271 | } |
| 248 | 272 | ||
| 249 | yield* orchestrationEngine.dispatch({ | 273 | yield* orchestrationEngine.dispatch({ |
| ... | @@ -261,7 +285,7 @@ const make = Effect.gen(function* () { | ... | @@ -261,7 +285,7 @@ const make = Effect.gen(function* () { |
| 261 | const assistantMessageId = MessageId.makeUnsafe(`assistant:${event.itemId}`); | 285 | const assistantMessageId = MessageId.makeUnsafe(`assistant:${event.itemId}`); |
| 262 | const turnId = toTurnId(event.turnId); | 286 | const turnId = toTurnId(event.turnId); |
| 263 | if (turnId) { | 287 | if (turnId) { |
| 264 | rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); | 288 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); |
| 265 | } | 289 | } |
| 266 | 290 | ||
| 267 | yield* orchestrationEngine.dispatch({ | 291 | yield* orchestrationEngine.dispatch({ |
| ... | @@ -274,14 +298,14 @@ const make = Effect.gen(function* () { | ... | @@ -274,14 +298,14 @@ const make = Effect.gen(function* () { |
| 274 | }); | 298 | }); |
| 275 | 299 | ||
| 276 | if (turnId) { | 300 | if (turnId) { |
| 277 | forgetAssistantMessageId(event.sessionId, turnId, assistantMessageId); | 301 | yield* forgetAssistantMessageId(event.sessionId, turnId, assistantMessageId); |
| 278 | } | 302 | } |
| 279 | } | 303 | } |
| 280 | 304 | ||
| 281 | if (event.type === "turn.completed") { | 305 | if (event.type === "turn.completed") { |
| 282 | const turnId = toTurnId(event.turnId); | 306 | const turnId = toTurnId(event.turnId); |
| 283 | if (turnId) { | 307 | if (turnId) { |
| 284 | const assistantMessageIds = getAssistantMessageIdsForTurn(event.sessionId, turnId); | 308 | const assistantMessageIds = yield* getAssistantMessageIdsForTurn(event.sessionId, turnId); |
| 285 | yield* Effect.forEach(assistantMessageIds, (assistantMessageId) => | 309 | yield* Effect.forEach(assistantMessageIds, (assistantMessageId) => |
| 286 | orchestrationEngine.dispatch({ | 310 | orchestrationEngine.dispatch({ |
| 287 | type: "thread.message.assistant.complete", | 311 | type: "thread.message.assistant.complete", |
| ... | @@ -292,12 +316,12 @@ const make = Effect.gen(function* () { | ... | @@ -292,12 +316,12 @@ const make = Effect.gen(function* () { |
| 292 | createdAt: now, | 316 | createdAt: now, |
| 293 | }), | 317 | }), |
| 294 | ).pipe(Effect.asVoid); | 318 | ).pipe(Effect.asVoid); |
| 295 | clearAssistantMessageIdsForTurn(event.sessionId, turnId); | 319 | yield* clearAssistantMessageIdsForTurn(event.sessionId, turnId); |
| 296 | } | 320 | } |
| 297 | } | 321 | } |
| 298 | 322 | ||
| 299 | if (event.type === "session.exited") { | 323 | if (event.type === "session.exited") { |
| 300 | clearTurnStateForSession(event.sessionId); | 324 | yield* clearTurnStateForSession(event.sessionId); |
| 301 | } | 325 | } |
| 302 | 326 | ||
| 303 | if (event.type === "runtime.error") { | 327 | if (event.type === "runtime.error") { |
apps/server/src/provider/Layers/ProviderService.ts+7-17| ... | @@ -12,7 +12,6 @@ | ... | @@ -12,7 +12,6 @@ |
| 12 | import { | 12 | import { |
| 13 | NonNegativeInt, | 13 | NonNegativeInt, |
| 14 | ProviderSessionId, | 14 | ProviderSessionId, |
| 15 | ProviderThreadId, | ||
| 16 | ThreadId, | 15 | ThreadId, |
| 17 | ProviderInterruptTurnInput, | 16 | ProviderInterruptTurnInput, |
| 18 | ProviderRespondToRequestInput, | 17 | ProviderRespondToRequestInput, |
| ... | @@ -94,14 +93,6 @@ function toRuntimePayloadFromSession(session: ProviderSession): Record<string, u | ... | @@ -94,14 +93,6 @@ function toRuntimePayloadFromSession(session: ProviderSession): Record<string, u |
| 94 | }; | 93 | }; |
| 95 | } | 94 | } |
| 96 | 95 | ||
| 97 | function asProviderSessionId(value: string | ProviderSessionId): ProviderSessionId { | ||
| 98 | return typeof value === "string" ? ProviderSessionId.makeUnsafe(value) : value; | ||
| 99 | } | ||
| 100 | |||
| 101 | function asProviderThreadId(value: string | ProviderThreadId): ProviderThreadId { | ||
| 102 | return typeof value === "string" ? ProviderThreadId.makeUnsafe(value) : value; | ||
| 103 | } | ||
| 104 | |||
| 105 | const makeProviderService = (options?: ProviderServiceLiveOptions) => | 96 | const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 106 | Effect.gen(function* () { | 97 | Effect.gen(function* () { |
| 107 | const canonicalEventLogger = | 98 | const canonicalEventLogger = |
| ... | @@ -126,7 +117,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -126,7 +117,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 126 | if (liveSessionId === event.sessionId) { | 117 | if (liveSessionId === event.sessionId) { |
| 127 | return { | 118 | return { |
| 128 | ...event, | 119 | ...event, |
| 129 | sessionId: ProviderSessionId.makeUnsafe(staleSessionId), | 120 | sessionId: staleSessionId, |
| 130 | } satisfies ProviderRuntimeEvent; | 121 | } satisfies ProviderRuntimeEvent; |
| 131 | } | 122 | } |
| 132 | } | 123 | } |
| ... | @@ -162,9 +153,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -162,9 +153,8 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 162 | ); | 153 | ); |
| 163 | } | 154 | } |
| 164 | 155 | ||
| 165 | const providerSessionId = asProviderSessionId(session.sessionId); | ||
| 166 | yield* directory.upsert({ | 156 | yield* directory.upsert({ |
| 167 | sessionId: providerSessionId, | 157 | sessionId: session.sessionId, |
| 168 | provider: session.provider, | 158 | provider: session.provider, |
| 169 | threadId, | 159 | threadId, |
| 170 | providerThreadId, | 160 | providerThreadId, |
| ... | @@ -444,7 +434,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -444,7 +434,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 444 | sessionId: routed.sessionId, | 434 | sessionId: routed.sessionId, |
| 445 | }); | 435 | }); |
| 446 | const threadId = yield* directory | 436 | const threadId = yield* directory |
| 447 | .getThreadId(asProviderSessionId(input.sessionId)) | 437 | .getThreadId(input.sessionId) |
| 448 | .pipe(Effect.map(Option.getOrUndefined)); | 438 | .pipe(Effect.map(Option.getOrUndefined)); |
| 449 | if (!threadId) { | 439 | if (!threadId) { |
| 450 | return yield* toValidationError( | 440 | return yield* toValidationError( |
| ... | @@ -453,10 +443,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -453,10 +443,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 453 | ); | 443 | ); |
| 454 | } | 444 | } |
| 455 | yield* directory.upsert({ | 445 | yield* directory.upsert({ |
| 456 | sessionId: asProviderSessionId(input.sessionId), | 446 | sessionId: input.sessionId, |
| 457 | provider: routed.adapter.provider, | 447 | provider: routed.adapter.provider, |
| 458 | threadId, | 448 | threadId, |
| 459 | providerThreadId: asProviderThreadId(turn.threadId), | 449 | providerThreadId: turn.threadId, |
| 460 | status: "running", | 450 | status: "running", |
| 461 | ...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}), | 451 | ...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}), |
| 462 | runtimePayload: { | 452 | runtimePayload: { |
| ... | @@ -514,10 +504,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -514,10 +504,10 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 514 | yield* routed.adapter.stopSession(routed.sessionId); | 504 | yield* routed.adapter.stopSession(routed.sessionId); |
| 515 | } | 505 | } |
| 516 | if (routed.sessionId !== input.sessionId) { | 506 | if (routed.sessionId !== input.sessionId) { |
| 517 | yield* directory.remove(asProviderSessionId(routed.sessionId)); | 507 | yield* directory.remove(routed.sessionId); |
| 518 | yield* clearAliasesReferencing(routed.sessionId); | 508 | yield* clearAliasesReferencing(routed.sessionId); |
| 519 | } | 509 | } |
| 520 | yield* directory.remove(asProviderSessionId(input.sessionId)); | 510 | yield* directory.remove(input.sessionId); |
| 521 | yield* clearAliasesReferencing(input.sessionId); | 511 | yield* clearAliasesReferencing(input.sessionId); |
| 522 | }); | 512 | }); |
| 523 | 513 |