Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ function createProviderServiceHarness(
},
}),
rollbackConversation,
hasOutstandingBackgroundTasks: () => Effect.succeed(false),
get streamEvents() {
return Stream.fromPubSub(runtimeEventPubSub);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ describe("ProviderCommandReactor", () => {
});
},
rollbackConversation: () => unsupported(),
hasOutstandingBackgroundTasks: () => Effect.succeed(false),
get streamEvents() {
return Stream.fromPubSub(runtimeEventPubSub);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ function createProviderServiceHarness() {
});
},
rollbackConversation: () => unsupported(),
hasOutstandingBackgroundTasks: () => Effect.succeed(false),
get streamEvents() {
return Stream.fromPubSub(runtimeEventPubSub);
},
Expand Down
50 changes: 50 additions & 0 deletions apps/server/src/persistence/ProviderSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInp
export const DeleteProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });
export type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type;

export const TouchLastSeenInput = Schema.Struct({
threadId: ThreadId,
lastSeenAt: IsoDateTime,
});
export type TouchLastSeenInput = typeof TouchLastSeenInput.Type;

/**
* ProviderSessionRuntimeRepository - Service tag for provider runtime persistence.
*/
Expand Down Expand Up @@ -93,6 +99,21 @@ export class ProviderSessionRuntimeRepository extends Context.Service<
ProviderSessionRuntimeRepositoryError
>;

/**
* Bump only `last_seen_at` for an existing, non-stopped row.
*
* A targeted update used to keep a session's inactivity clock fresh from
* background runtime activity (e.g. a running dynamic workflow) without
* rewriting the full runtime payload. Rows in `stopped` status are left
* untouched so a reaped session is never resurrected.
*
* Returns whether a live row was actually updated, so callers can tell a
* real refresh from a no-op against an absent or stopped row.
*/
readonly touchLastSeen: (
input: TouchLastSeenInput,
) => Effect.Effect<boolean, ProviderSessionRuntimeRepositoryError>;

/**
* Delete provider runtime state by canonical thread id.
*/
Expand Down Expand Up @@ -226,6 +247,22 @@ export const make = Effect.gen(function* () {
`,
});

// `RETURNING` reports which rows the update actually matched: an absent or
// already-stopped row yields none, and the caller needs that distinction to
// avoid treating a no-op as a successful refresh.
const touchLastSeenRow = SqlSchema.findAll({
Request: TouchLastSeenInput,
Result: Schema.Struct({ threadId: Schema.String }),
execute: ({ threadId, lastSeenAt }) =>
sql`
UPDATE provider_session_runtime
SET last_seen_at = ${lastSeenAt}
WHERE thread_id = ${threadId}
AND status != 'stopped'
RETURNING thread_id AS "threadId"
`,
});

const deleteRuntimeByThreadId = SqlSchema.void({
Request: DeleteRuntimeRequestSchema,
execute: ({ threadId }) =>
Expand Down Expand Up @@ -308,6 +345,18 @@ export const make = Effect.gen(function* () {
),
);

const touchLastSeen: ProviderSessionRuntimeRepository["Service"]["touchLastSeen"] = (input) =>
touchLastSeenRow(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"ProviderSessionRuntimeRepository.touchLastSeen:query",
"ProviderSessionRuntimeRepository.touchLastSeen:encodeRequest",
{ threadId: input.threadId },
),
),
Effect.map((rows) => rows.length > 0),
);

const deleteByThreadId: ProviderSessionRuntimeRepository["Service"]["deleteByThreadId"] = (
input,
) =>
Expand All @@ -326,6 +375,7 @@ export const make = Effect.gen(function* () {
upsert,
getByThreadId,
list,
touchLastSeen,
deleteByThreadId,
} satisfies ProviderSessionRuntimeRepository["Service"];
});
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory
getBinding: () => Effect.succeed(Option.none()),
listThreadIds: () => Effect.succeed([]),
listBindings: () => Effect.succeed([]),
touchLastSeen: () => Effect.succeed(true),
});

const validationRuntimeFactory = makeRuntimeFactory();
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory
getBinding: () => Effect.succeed(Option.none()),
listThreadIds: () => Effect.succeed([]),
listBindings: () => Effect.succeed([]),
touchLastSeen: () => Effect.succeed(true),
});

// The adapter now receives its settings as a plain argument (the old design
Expand Down
Loading
Loading