authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 10:35:37-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 13:20:36-08:00
log8efbb31725f8500e1aff76655b04a3fab7b064fe
tree9f7853eaf32211f7576ba49ca511caa208927bfe
parentb4c74723d1a3072551e38d1bd3b5098e3872a51d

use effect/Cache instead of ad-hoc map caches


4 files changed, 155 insertions(+), 151 deletions(-)

apps/server/src/git/Layers/GitCore.ts+67-54
...@@ -1,11 +1,19 @@...@@ -1,11 +1,19 @@
1import { Effect, FileSystem, Layer, Path } from "effect";1import { Cache, Data, Duration, Effect, Exit, FileSystem, Layer, Path } from "effect";
22
3import { GitCommandError } from "../Errors.ts";3import { GitCommandError } from "../Errors.ts";
4import { GitService } from "../Services/GitService.ts";4import { GitService } from "../Services/GitService.ts";
5import { GitCore, type GitCoreShape } from "../Services/GitCore.ts";5import { GitCore, type GitCoreShape } from "../Services/GitCore.ts";
66
7const STATUS_UPSTREAM_REFRESH_INTERVAL_MS = 15_000;7const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);
8const STATUS_UPSTREAM_REFRESH_TIMEOUT_MS = 5_000;8const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);
9const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048;
10
11class StatusUpstreamRefreshCacheKey extends Data.Class<{
12 cwd: string;
13 upstreamRef: string;
14 remoteName: string;
15 upstreamBranch: string;
16}> {}
917
10interface ExecuteGitOptions {18interface 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}
1523
16function trimStdout(value: string): string {
17 return value.trim();
18}
19
20function parseBranchAb(value: string): { ahead: number; behind: number } {24function 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>();
100103
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 GitCommandError165 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 };
222224
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,
229246 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 });
241254
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 });
250261
...@@ -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 }
401412
...@@ -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 });
428441
...@@ -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* () {
540555
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 );
548561
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";
11import { Effect, Layer, Queue, Stream } from "effect";11import { Cache, Duration, Effect, Layer, Option, Queue, Stream } from "effect";
1212
13import { ProviderService } from "../../provider/Services/ProviderService.ts";13import { ProviderService } from "../../provider/Services/ProviderService.ts";
14import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";14import { 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()}`);
7777
78const HANDLED_TURN_START_KEY_MAX = 10_000;78const HANDLED_TURN_START_KEY_MAX = 10_000;
79const HANDLED_TURN_START_KEY_TTL_MS = 30 * 60 * 1_000;79const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);
80
81function 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
99function 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}
11480
115const make = Effect.gen(function* () {81const 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 );
11996
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 }
252229
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";
10import { Cause, Effect, Layer, Queue, Stream } from "effect";10import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect";
1111
12import { ProviderService } from "../../provider/Services/ProviderService.ts";12import { ProviderService } from "../../provider/Services/ProviderService.ts";
13import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";13import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
...@@ -19,6 +19,8 @@ import {...@@ -19,6 +19,8 @@ import {
19const providerTurnKey = (sessionId: ProviderSessionId, turnId: TurnId) => `${sessionId}:${turnId}`;19const providerTurnKey = (sessionId: ProviderSessionId, turnId: TurnId) => `${sessionId}:${turnId}`;
20const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId =>20const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId =>
21 CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`);21 CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`);
22const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000;
23const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120);
2224
23function toTurnId(value: string | undefined): TurnId | undefined {25function 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;
130132
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 });
132138
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 );
146160
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 );
162181
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 );
166188
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 };
170191
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 });
179203
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 }
248272
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 }
266290
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 });
275299
276 if (turnId) {300 if (turnId) {
277 forgetAssistantMessageId(event.sessionId, turnId, assistantMessageId);301 yield* forgetAssistantMessageId(event.sessionId, turnId, assistantMessageId);
278 }302 }
279 }303 }
280304
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 }
298322
299 if (event.type === "session.exited") {323 if (event.type === "session.exited") {
300 clearTurnStateForSession(event.sessionId);324 yield* clearTurnStateForSession(event.sessionId);
301 }325 }
302326
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 @@
12import {12import {
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}
9695
97function asProviderSessionId(value: string | ProviderSessionId): ProviderSessionId {
98 return typeof value === "string" ? ProviderSessionId.makeUnsafe(value) : value;
99}
100
101function asProviderThreadId(value: string | ProviderThreadId): ProviderThreadId {
102 return typeof value === "string" ? ProviderThreadId.makeUnsafe(value) : value;
103}
104
105const makeProviderService = (options?: ProviderServiceLiveOptions) =>96const 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 }
164155
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* directory436 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 });
523513