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