Compare commits

...

2 Commits

Author SHA1 Message Date
Kit Langton d750d51a3d fix(core): retry failed session wakes 2026-06-06 08:09:36 -04:00
Kit Langton ff5874f812 fix(core): bound prompt cache session keys 2026-06-05 23:11:45 -04:00
4 changed files with 94 additions and 7 deletions
+15 -2
View File
@@ -46,6 +46,7 @@ type Entry<A, E> = {
interruptSeq?: number
owner?: Fiber.Fiber<void, never>
stopping: boolean
advisoryRetriesRemaining: number
}
/** Combines follow-up demand: runs dominate, while wakes retain the newest durable admission sequence. */
@@ -81,12 +82,17 @@ export const make = <Key, A, E>(options: {
}),
)
const makeEntry = (current: Demand, explicitWaiter?: Deferred.Deferred<A, E>): Entry<A, E> => ({
const makeEntry = (
current: Demand,
explicitWaiter?: Deferred.Deferred<A, E>,
advisoryRetriesRemaining = current._tag === "wake" ? 1 : 0,
): Entry<A, E> => ({
done: Deferred.makeUnsafe<A, E>(),
settled: Deferred.makeUnsafe<Exit.Exit<A, E>>(),
current,
explicitWaiter,
stopping: false,
advisoryRetriesRemaining,
})
const start = (key: Key, entry: Entry<A, E>, demand: Demand, successor = false) => {
@@ -132,6 +138,7 @@ export const make = <Key, A, E>(options: {
const pending = entry.pending
entry.pending = undefined
entry.current = pending
entry.advisoryRetriesRemaining = pending._tag === "wake" ? 1 : 0
start(key, entry, pending, true)
return
}
@@ -141,7 +148,12 @@ export const make = <Key, A, E>(options: {
return
}
const successor = entry.pending !== undefined ? makeEntry(entry.pending, entry.explicitWaiter) : undefined
const successor =
entry.pending !== undefined
? makeEntry(entry.pending, entry.explicitWaiter)
: exit._tag === "Failure" && demand._tag === "wake" && !entry.stopping && entry.advisoryRetriesRemaining > 0
? makeEntry(demand, entry.explicitWaiter, entry.advisoryRetriesRemaining - 1)
: undefined
if (successor === undefined) active.delete(key)
else active.set(key, successor)
if (successor !== undefined) start(key, successor, successor.current, true)
@@ -151,6 +163,7 @@ export const make = <Key, A, E>(options: {
exit._tag === "Failure" &&
!(entry.stopping && Cause.hasInterruptsOnly(exit.cause)) &&
demand._tag === "wake" &&
successor === undefined &&
options.onFailure !== undefined
) {
report(Effect.suspend(() => options.onFailure!(key, exit.cause)))
+2 -1
View File
@@ -211,9 +211,10 @@ export const layer = Layer.effect(
const model = yield* models.resolve(session)
const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq)
const context = entries.map((entry) => entry.message)
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
const request = LLM.request({
model,
providerOptions: { openai: { promptCacheKey: session.id } },
providerOptions: { openai: { promptCacheKey } },
system: [agent.info?.system, system.baseline]
.filter((part): part is string => part !== undefined && part.length > 0)
.map(SystemPart.make),
@@ -33,7 +33,9 @@ describe("SessionRunCoordinator", () => {
Effect.scoped(
Effect.gen(function* () {
const drained = yield* Deferred.make<void>()
const coordinator = yield* SessionRunCoordinator.make({ drain: () => Deferred.succeed(drained, undefined) })
const coordinator = yield* SessionRunCoordinator.make({
drain: () => Deferred.succeed(drained, undefined),
})
yield* coordinator.wake("session")
yield* Deferred.await(drained)
@@ -44,7 +46,9 @@ describe("SessionRunCoordinator", () => {
it.effect("does nothing when interrupted while idle", () =>
Effect.scoped(
Effect.gen(function* () {
const coordinator = yield* SessionRunCoordinator.make({ drain: () => Effect.void })
const coordinator = yield* SessionRunCoordinator.make({
drain: () => Effect.void,
})
yield* coordinator.interrupt("session")
}),
@@ -55,7 +59,9 @@ describe("SessionRunCoordinator", () => {
Effect.scoped(
Effect.gen(function* () {
let runs = 0
const coordinator = yield* SessionRunCoordinator.make({ drain: () => Effect.sync(() => runs++) })
const coordinator = yield* SessionRunCoordinator.make({
drain: () => Effect.sync(() => runs++),
})
yield* coordinator.interrupt("session", 2)
yield* coordinator.wake("session", 1)
@@ -722,6 +728,35 @@ describe("SessionRunCoordinator", () => {
),
)
it.effect("retries a failed advisory successor once without another wake", () =>
Effect.scoped(
Effect.gen(function* () {
const firstGate = yield* Deferred.make<void>()
let runs = 0
const failure = new Error("transient wake failure")
const coordinator = yield* SessionRunCoordinator.make<string, void, Error>({
drain: () =>
Effect.sync(() => ++runs).pipe(
Effect.flatMap((run) =>
run === 1
? Deferred.await(firstGate).pipe(Effect.andThen(Effect.fail(failure)))
: run === 2
? Effect.fail(failure)
: Effect.void,
),
),
})
yield* coordinator.wake("session", 1)
yield* coordinator.wake("session", 2)
yield* Deferred.succeed(firstGate, undefined)
yield* coordinator.awaitIdle("session").pipe(Effect.exit)
expect(runs).toBe(3)
}),
),
)
it.effect("upgrades an active wake when an explicit run joins it", () =>
Effect.scoped(
Effect.gen(function* () {
@@ -916,8 +951,9 @@ describe("SessionRunCoordinator", () => {
const failure = new Error("wake failed")
const reported: Cause.Cause<Error>[] = []
const reportedOnce = yield* Deferred.make<void>()
let runs = 0
const coordinator = yield* SessionRunCoordinator.make<string, void, Error>({
drain: () => Effect.fail(failure),
drain: () => Effect.sync(() => runs++).pipe(Effect.andThen(Effect.fail(failure))),
onFailure: (_key, cause) =>
Effect.sync(() => reported.push(cause)).pipe(Effect.andThen(Deferred.succeed(reportedOnce, undefined))),
})
@@ -927,6 +963,7 @@ describe("SessionRunCoordinator", () => {
yield* Effect.yieldNow
expect(reported).toHaveLength(1)
expect(runs).toBe(2)
expect(Cause.squash(reported[0]!)).toBe(failure)
}),
),
+36
View File
@@ -2824,6 +2824,42 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("bounds external session prompt cache keys", () =>
Effect.gen(function* () {
yield* setup
const externalSessionID = SessionV2.ID.fromExternal({
namespace: "discord",
key: "thread-one",
})
const otherExternalSessionID = SessionV2.ID.fromExternal({
namespace: "discord",
key: "thread-two",
})
yield* insertSession(externalSessionID)
yield* insertSession(otherExternalSessionID)
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID: externalSessionID,
prompt: new Prompt({ text: "Run external session" }),
resume: false,
})
yield* session.prompt({
sessionID: otherExternalSessionID,
prompt: new Prompt({ text: "Run other external session" }),
resume: false,
})
requests.length = 0
yield* session.resume(externalSessionID)
yield* session.resume(otherExternalSessionID)
const keys = requests.map((request) => request.providerOptions?.openai?.promptCacheKey)
expect(keys).toEqual([externalSessionID.slice(4), otherExternalSessionID.slice(4)])
expect(keys.every((key) => typeof key === "string" && key.length === 64)).toBe(true)
expect(keys[0]).not.toBe(keys[1])
}),
)
it.effect("fans out one failed run and allows a later retry", () =>
Effect.gen(function* () {
yield* setup