Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
77e35c5
fix(web): send cited messages with Cmd+Enter (#9307)
extoci Sep 3, 2026
098bf53
fix(web): preserve explicit preview navigation URLs (#8902)
nateEc Sep 3, 2026
57626eb
fix(web): prevent loading ssh environments from overriding navigation…
flamboh Sep 3, 2026
cfddb42
fix(mobile): skip unsupported shared settings targets (#9381)
Lucenx9 Sep 3, 2026
2120fbc
fix(web): avoid duplicate Antigravity install status (#9419)
RakshithBhat03 Sep 3, 2026
d4ba2a1
fix(composer): mute fast icon when collapsed (#9451)
maria-rcks Sep 3, 2026
21b9dda
fix(web): unify skeleton loading animations on one pulse (#9448)
maria-rcks Sep 3, 2026
645d585
fix(web): prioritize authored pull requests (#9453)
maria-rcks Sep 3, 2026
4e89d74
fix(web): make project icons the default (#9457)
maria-rcks Sep 3, 2026
c78f05a
fix(server): reuse pr state when settling threads (#9459)
maria-rcks Sep 3, 2026
8bd544c
fix(web): keep agent images collapsed (#9460)
maria-rcks Sep 3, 2026
126afb5
fix(web): banner buttons no longer expand the resting composer (#9452)
juliusmarminge Sep 3, 2026
d5825e1
fix(web): stop clipping the traits chevron on long Codex effort label…
zortos293 Sep 3, 2026
46e8b1a
fix(web): make right panel tabs easier to scroll (#9461)
maria-rcks Sep 3, 2026
db8d60f
fix(web): render transparent previews on white (#9463)
UtkarshUsername Sep 3, 2026
de025aa
fix(mobile): show loading and syncing in the working pill (#9466)
juliusmarminge Sep 3, 2026
36c4e9c
fix(server): keep a/ and b/ prefixes in rendered git patches (#9438)
Mnigos Sep 3, 2026
d2b6f3b
fix(server): full-access OpenCode threads no longer ask for approvals…
shivamhwp Sep 3, 2026
493fbb5
fix(web): reuse pull request list data while loading (#9467)
maria-rcks Sep 3, 2026
0372836
feat(web): let users turn off composer collapse on blur and scroll (#…
juliusmarminge Sep 3, 2026
373be93
fix(web): move workflow approval beside checks (#9465)
maria-rcks Sep 3, 2026
4b8b5d9
fix(desktop): refresh generated annotation styles (#9488)
juliusmarminge Sep 3, 2026
0869ad6
fix(web): let the PR reviewer and label search boxes take keystrokes …
juliusmarminge Sep 3, 2026
77138cf
fix(web): dont collapse composer when interacting with bottom row (#9…
extoci Sep 3, 2026
678f23a
fix(desktop): restore second-press quit fallback (#9485)
maria-rcks Sep 3, 2026
c726c30
fix(web): keep opencode icon hollow in collapsed composer (#9492)
maria-rcks Sep 3, 2026
12e8997
fix(web): keep agent browser preview visible (#9484)
maria-rcks Sep 3, 2026
409bc4f
fix(mobile): keep the machine glyph next to the environment label (#9…
juliusmarminge Sep 3, 2026
80b5373
fix(mobile): let back swipe pop from horizontal scroll edges (#9493)
juliusmarminge Sep 3, 2026
0aae1e2
fix(antigravity): discover legacy workspace skills (#9410)
Lucenx9 Sep 3, 2026
ef4cc60
fix(mobile): resolve Antigravity provider icon and normalize driver m…
Invictine Sep 3, 2026
e01c153
fix(antigravity): forward Google sign-in URLs from browser helper (#9…
WellyngtonF Sep 3, 2026
39449e5
feat(desktop): import browser cookies into a profile (#7255)
juliusmarminge Sep 3, 2026
ff58434
feat(desktop): import from Chrome, Edge, Brave, Vivaldi, Opera, Arc a…
juliusmarminge Sep 3, 2026
498ab9c
feat(desktop): resolve Chromium cookie keys on Linux (#7261)
juliusmarminge Sep 3, 2026
f25e442
fix(antigravity): allow slow runtime startup during setup (#9510)
t3dotgg Sep 3, 2026
baf67b6
fix(antigravity): keep model choices up to date (#9511)
t3dotgg Sep 3, 2026
eb334ca
fix(antigravity): handle native sign-in URLs on stderr (#9514)
t3dotgg Sep 3, 2026
8ea52c8
fix(antigravity): update managed runtime to 1.1.1 (#9509)
t3dotgg Sep 3, 2026
3653cb2
fix(desktop): address the browser import review left over from the st…
juliusmarminge Sep 3, 2026
f8a14b2
feat(antigravity): show subagent calls and results (#9515)
t3dotgg Sep 4, 2026
6382268
fix(web): let paste expand a resting composer (#9498)
juliusmarminge Sep 4, 2026
522ebe6
fix(web): keep the composer open while selecting timeline text (#9499)
juliusmarminge Sep 4, 2026
c0ebc88
fix(web): return focus to the composer after closing a media preview …
juliusmarminge Sep 4, 2026
9c9ae3d
fix(server): keep events during thread subscription startup (#9521)
t3dotgg Sep 4, 2026
44701ef
chore: forward issue/PR/discussion events to Cursor hygiene (#9518)
juliusmarminge Sep 4, 2026
9d28c21
fix(auth): keep pairing credentials out of access read models (#9523)
t3dotgg Sep 4, 2026
e3723e0
chore: drop comment events from Cursor hygiene forwarder (#9527)
juliusmarminge Sep 4, 2026
d76b24d
feat(codex): support async questions (#9512)
t3dotgg Sep 4, 2026
0a0b6be
fix(web): keep right panel controls clickable (#9517)
maria-rcks Sep 4, 2026
19d8ab2
feat(usage): show Codex and Claude subscription limits on a Limits ta…
juliusmarminge Sep 4, 2026
343db2c
feat(web): reorganize settings pages (#9354)
maria-rcks Sep 4, 2026
2b96220
fix(server): settle branch threads immediately on pull request merge …
maria-rcks Sep 4, 2026
b908980
fix(server): back off relay client restarts after rapid exits (#8788)
derektrimm Sep 4, 2026
75ab5ab
fix(codex): accept rate limit errors on thread resume (#8897)
nateEc Sep 4, 2026
6319a97
fix(desktop): preview CDP sessions no longer hard-crash the app (#9068)
sethwebster Sep 4, 2026
f54ab90
Fix worktree removal timing out on large install trees (#3902)
jakeleventhal Sep 4, 2026
0ba06a1
fix(web): settle the resting composer layout with a pixel of slack (#…
matheustimbo Sep 4, 2026
07c4ab5
fix(web): keep automatic project icons consistent (#9535)
t3dotgg Sep 4, 2026
dddc0bd
fix(server): include SQLite conditions in persistence errors
t3dotgg Sep 4, 2026
6f40537
fix(dev): keep shared dev reloads and hot updates working (#9543)
t3dotgg Sep 4, 2026
c5ba51d
feat(providers): add context compaction command (#9293)
maria-rcks Sep 4, 2026
5989de4
fix(mobile): keep store screenshots free of system banners and show d…
juliusmarminge Sep 4, 2026
54aef6f
fix(web): restore composer controls as space becomes available (#9539)
juliusmarminge Sep 4, 2026
3c3e05c
fix(web): measure collapsed model labels at their visible width (#9540)
juliusmarminge Sep 4, 2026
f239b77
fix(web): close composer menus when their controls hide (#9541)
juliusmarminge Sep 4, 2026
3e2c1a6
fix(web): thread error banner no longer shifts the chat (#9473)
flamboh Sep 4, 2026
617edab
fix(server): reveal normalized paths in File Explorer (#9551)
juliusmarminge Sep 4, 2026
232de5e
feat(marketing): fresh screenshot and floating marks on the homepage …
t3dotgg Sep 4, 2026
1641b4a
feat(usage): redeem Codex reset credits from the Limits tab (#9534)
juliusmarminge Sep 4, 2026
4e54731
fix(server): find newly opened pull requests after agent turns (#9125)
t3dotgg Sep 4, 2026
f96a220
ci: add on-demand Windows test workflow (#9538)
juliusmarminge Sep 4, 2026
710f6dc
fix(web): simplify expanded tool details (#9549)
maria-rcks Sep 4, 2026
9e1bc36
fix(web): keep the last message visible when the resting composer exp…
t3dotgg Sep 4, 2026
fee2e0f
test(web): fix flaky startup and Tailwind tests (#9558)
t3dotgg Sep 4, 2026
65f1839
fix(web): keep codex restart responses continuous (#9560)
maria-rcks Sep 4, 2026
d7884ce
fix(web): make settings sidebar sub-section buttons full width (#9562)
juliusmarminge Sep 4, 2026
2b10398
fix(web): render settings sidebar immediately (#9563)
maria-rcks Sep 4, 2026
95390ed
chore: vouch august contributors (#9557)
maria-rcks Sep 4, 2026
42bdea1
fix(web): stabilize right panel transitions (#9554)
maria-rcks Sep 4, 2026
0cb02ab
fix: better shell syntax handling for labels (#9371)
Yash-Singh1 Sep 4, 2026
bf40fa7
fix(web): align the sidebar wordmark by baseline (#9578)
maria-rcks Sep 4, 2026
2675e3c
fix(antigravity): keep subagent batches active after launch (#9579)
t3dotgg Sep 4, 2026
09b81a3
fix(mobile): render workspace images in markdown file previews (#8769)
SunkenInTime Sep 4, 2026
b34ff8f
fix(usage): deduplicate CLI proxy subscription accounts (#9584)
juliusmarminge Sep 4, 2026
07891e9
fix(web): bound disconnected send toasts (#9592)
maria-rcks Sep 4, 2026
5783280
fix(desktop): restore panel titlebar interactions (#9591)
maria-rcks Sep 4, 2026
39abb9d
fix(connect): refresh authorization without disconnecting (#9582)
maria-rcks Sep 4, 2026
f559fe0
fix(web): show context meter in compact composer (#9430)
GuilhermeVieiraDev Sep 4, 2026
5cc369b
fix(pull-requests): refresh data after thread turns (#9496)
maria-rcks Sep 4, 2026
caab2fd
fix(web): render draft PRs in gray (#9537)
extoci Sep 4, 2026
f1e90e3
refactor(web): move usage provider controls to settings (#9599)
juliusmarminge Sep 4, 2026
00f8b7c
fix: show idle subagent batches without completion marks (#9616)
t3dotgg Sep 4, 2026
61a91b6
fix(web): group image views like other tool calls (#9597)
juliusmarminge Sep 4, 2026
c3b8825
fix: preserve tool icons on failed calls (#9606)
juliusmarminge Sep 4, 2026
99e3b72
fix(connect): diagnose incomplete headless server setup (#9602)
juliusmarminge Sep 4, 2026
4cc800c
fix(web): keep command palette above composer menus (#9613)
Gigioxx Sep 4, 2026
93c3ab4
fix(web): snooze menu no longer overlaps thread details (#9601)
RakshithBhat03 Sep 4, 2026
7062315
fix(web): match composer pull request state icons (#9375)
flamboh Sep 4, 2026
2152d44
fix(server): load OpenCode workspace skills via SDK to avoid 64KB CLI…
BarryHenryJr Sep 4, 2026
ec3ec6f
fix(web): mute sidebar branch name to match worktree icon (#9622)
maria-rcks Sep 4, 2026
5f878d2
fix(web,mobile): fold context compaction under settled turn folds (#9…
maria-rcks Sep 4, 2026
09d13de
feat(mobile): make chat text selectable on Android (#8779)
SunkenInTime Sep 4, 2026
14bf3f6
fix(web): toggle a single stashed prompt with Cmd+S (#9644)
t3dotgg Sep 4, 2026
eb77683
fix(server): prevent duplicate desktop clients after restart
seeb1337 Sep 4, 2026
d487dfb
fix(web): resume Antigravity threads without repeated sign-in (#9647)
t3dotgg Sep 4, 2026
d5b9410
feat(mobile): paste the phone clipboard into the terminal (#9199)
lnieuwenhuis Sep 4, 2026
f034732
feat(web): show which sidebar threads hold an unsent draft (#9658)
t3dotgg Sep 4, 2026
01f3e50
fix(server): unblock OpenCode approvals and stop (#9653)
t3dotgg Sep 4, 2026
caa8a0d
fix(desktop): quit immediately on a second shortcut press (#9657)
t3dotgg Sep 4, 2026
560afff
fix(server): update Claude Agent SDK to 0.3.260 (#9135)
t3dotgg Sep 4, 2026
8ac5462
perf(server): stop loading message bodies for thread summaries (#9662)
t3dotgg Sep 4, 2026
cccd7e3
perf(web): speed up terminal snapshots (#9663)
t3dotgg Sep 4, 2026
82da289
Sync coder-only with main through 2026-09-04
henrychu04 Sep 4, 2026
67d4209
Merge latest coder-only changes into upstream sync
henrychu04 Sep 4, 2026
6f0b13f
Complete upstream sync adaptations and validate Claude authentication
henrychu04 Sep 5, 2026
4a3c35b
Fix reviewed Coder sync regressions and add focused coverage
henrychu04 Sep 5, 2026
9400d60
Include orchestration regression suites in Coder tests
henrychu04 Sep 5, 2026
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
5 changes: 5 additions & 0 deletions apps/server/src/checkpointing/CheckpointDiffQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe("CheckpointDiffQuery.layer", () => {
Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
getCommandReadModel: () =>
Effect.die("CheckpointDiffQuery should not request the command read model"),
getUserInputActivity: () => Effect.die("unused"),
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getShellSnapshot: () =>
Expand Down Expand Up @@ -186,6 +187,7 @@ describe("CheckpointDiffQuery.layer", () => {
Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
getCommandReadModel: () =>
Effect.die("CheckpointDiffQuery should not request the command read model"),
getUserInputActivity: () => Effect.die("unused"),
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getShellSnapshot: () =>
Expand Down Expand Up @@ -270,6 +272,7 @@ describe("CheckpointDiffQuery.layer", () => {
Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
getCommandReadModel: () =>
Effect.die("CheckpointDiffQuery should not request the command read model"),
getUserInputActivity: () => Effect.die("unused"),
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getShellSnapshot: () =>
Expand Down Expand Up @@ -339,6 +342,7 @@ describe("CheckpointDiffQuery.layer", () => {
Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
getCommandReadModel: () =>
Effect.die("CheckpointDiffQuery should not request the command read model"),
getUserInputActivity: () => Effect.die("unused"),
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getShellSnapshot: () =>
Expand Down Expand Up @@ -393,6 +397,7 @@ describe("CheckpointDiffQuery.layer", () => {
Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
getCommandReadModel: () =>
Effect.die("CheckpointDiffQuery should not request the command read model"),
getUserInputActivity: () => Effect.die("unused"),
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getShellSnapshot: () =>
Expand Down
31 changes: 31 additions & 0 deletions apps/server/src/checkpointing/CheckpointStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,37 @@ it.layer(TestLayer)("CheckpointStore.layer", (it) => {
}),
);

it.effect("keeps a/ and b/ patch prefixes when the repository disables them", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
yield* git(tmp, ["config", "diff.noprefix", "true"]);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-checkpoint-store-noprefix");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);

yield* checkpointStore.captureCheckpoint({
cwd: tmp,
checkpointRef: fromCheckpointRef,
});
yield* writeTextFile(NodePath.join(tmp, "README.md"), "# changed\n");
yield* checkpointStore.captureCheckpoint({
cwd: tmp,
checkpointRef: toCheckpointRef,
});

const diff = yield* checkpointStore.diffCheckpoints({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
ignoreWhitespace: false,
});

expect(diff).toContain("diff --git a/README.md b/README.md");
}),
);

it.effect("can hide indentation churn when changes wrap existing lines", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/coderWs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@ export const layer = CoderWsRpcGroup.toLayer(
pullRequests.setThreadResolution(input),
[WS_METHODS.pullRequestsSetReaction]: (input) => pullRequests.setReaction(input),
[WS_METHODS.pullRequestsInvalidate]: (input) => pullRequests.invalidate(input),
[WS_METHODS.pullRequestsSubscribeRefreshes]: () => pullRequests.subscribeRefreshes,
[WS_METHODS.pullRequestsReviewerCandidates]: (input) =>
pullRequests.reviewerCandidates(input),
[WS_METHODS.pullRequestsRequestReviewers]: (input) => pullRequests.requestReviewers(input),
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ export const layer = Layer.effect(
const STATUS_CACHE_TTL = Duration.seconds(1);
const STATUS_CACHE_CAPACITY = 2_048;
const FETCH_CACHE_TTL = Duration.seconds(1);
const PR_CACHE_TTL = Duration.minutes(2);
// Match the automatic settlement sweep cadence so an external merge is
// observed on the next sweep instead of waiting on an older cache entry.
const PR_CACHE_TTL = Duration.seconds(60);
const FAILURE_BASE_TTL = Duration.seconds(20);
const FAILURE_MAX_TTL = Duration.minutes(15);
const failureTtl = (failures: number) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { checkpointRefForThreadTurn } from "../../checkpointing/Utils.ts";
import { ServerConfig } from "../../config.ts";
import * as WorkspaceEntries from "../../workspace/WorkspaceEntries.ts";
import * as WorkspacePaths from "../../workspace/WorkspacePaths.ts";
import * as PullRequestService from "../../pullRequest/PullRequestService.ts";

const asProjectId = (value: string): ProjectId => ProjectId.make(value);
const asTurnId = (value: string): TurnId => TurnId.make(value);
Expand Down Expand Up @@ -107,6 +108,7 @@ function createProviderServiceHarness(
const service: ProviderServiceShape = {
startSession: () => unsupported(),
sendTurn: () => unsupported(),
compactThread: () => unsupported(),
interruptTurn: () => unsupported(),
respondToRequest: () => unsupported(),
respondToUserInput: () => unsupported(),
Expand Down Expand Up @@ -314,6 +316,8 @@ describe("CheckpointReactor", () => {
const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-checkpoint-reactor-test-",
});
const pullRequestRefreshes: number[] = [];
const refreshAfterTurn = Effect.sync(() => void pullRequestRefreshes.push(1));
const vcsStatusLayer = Layer.succeed(CoderVcsStatus, {
refresh: (cwd: string) =>
Effect.sync(() => {
Expand Down Expand Up @@ -343,6 +347,7 @@ describe("CheckpointReactor", () => {
Layer.provideMerge(projectionSnapshotLayer),
Layer.provideMerge(RuntimeReceiptBusLive),
Layer.provideMerge(Layer.succeed(ProviderService, provider.service)),
Layer.provideMerge(Layer.mock(PullRequestService.PullRequestService)({ refreshAfterTurn })),
Layer.provideMerge(vcsStatusLayer),
Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer))),
Layer.provideMerge(
Expand Down Expand Up @@ -454,6 +459,7 @@ describe("CheckpointReactor", () => {
provider,
cwd,
drain,
pullRequestRefreshes,
};
}

Expand Down Expand Up @@ -773,6 +779,7 @@ describe("CheckpointReactor", () => {
const midReadModel = await harness.readModel();
const midThread = midReadModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
expect(midThread?.checkpoints).toHaveLength(0);
expect(harness.pullRequestRefreshes).toEqual([]);

harness.provider.emit({
type: "turn.completed",
Expand All @@ -790,6 +797,8 @@ describe("CheckpointReactor", () => {
(entry) => entry.latestTurn?.turnId === "turn-main" && entry.checkpoints.length === 1,
);
expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1);
await harness.drain();
expect(harness.pullRequestRefreshes).toEqual([1]);
});

it("captures pre-turn and completion checkpoints for claude runtime events", async () => {
Expand Down
49 changes: 47 additions & 2 deletions apps/server/src/orchestration/Layers/CheckpointReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type { OrchestrationDispatchError } from "../Errors.ts";
import { isGitRepository } from "../../git/Utils.ts";
import { CoderVcsStatus } from "../../coderVcsStatus.ts";
import * as WorkspaceEntries from "../../workspace/WorkspaceEntries.ts";
import * as PullRequestService from "../../pullRequest/PullRequestService.ts";

const nowIso = Effect.map(DateTime.now, DateTime.formatIso);

Expand Down Expand Up @@ -96,6 +97,9 @@ const make = Effect.gen(function* () {
const receiptBus = yield* RuntimeReceiptBus;
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const vcsStatus = yield* CoderVcsStatus;
const pullRequests = yield* PullRequestService.PullRequestService;
const startedTurns = new Map<ThreadId, TurnId>();
const pendingTurns = new Set<ThreadId>();

const appendRevertFailureActivity = (input: {
readonly threadId: ThreadId;
Expand Down Expand Up @@ -881,6 +885,9 @@ const make = Effect.gen(function* () {

const processDomainEvent = Effect.fn("processDomainEvent")(function* (event: OrchestrationEvent) {
if (event.type === "thread.turn-start-requested" || event.type === "thread.message-sent") {
if (event.type === "thread.turn-start-requested") {
pendingTurns.add(event.payload.threadId);
}
yield* ensurePreTurnBaselineFromDomainTurnStart(event);
return;
}
Expand Down Expand Up @@ -925,7 +932,22 @@ const make = Effect.gen(function* () {
const processRuntimeEvent = Effect.fn("processRuntimeEvent")(function* (
event: ProviderRuntimeEvent,
) {
if (event.type === "session.exited") {
startedTurns.delete(event.threadId);
pendingTurns.delete(event.threadId);
return;
}

if (event.type === "turn.started") {
const turnId = toTurnId(event.turnId);
const activeTurnId = (yield* providerService.listSessions()).find((session) =>
sameId(session.threadId, event.threadId),
)?.activeTurnId;
const mayReplace = pendingTurns.has(event.threadId) && sameId(activeTurnId, turnId);
if (turnId !== null && (!startedTurns.has(event.threadId) || mayReplace)) {
startedTurns.set(event.threadId, turnId);
pendingTurns.delete(event.threadId);
}
yield* ensurePreTurnBaselineFromTurnStart(event);
return;
}
Expand All @@ -947,9 +969,30 @@ const make = Effect.gen(function* () {
return;
}

if (event.type === "turn.completed") {
if (event.type === "turn.completed" || event.type === "turn.aborted") {
const turnId = toTurnId(event.turnId);
yield* refreshLocalGitStatusAfterAgentCommand(event);
const thread = yield* resolveThreadDetail(event.threadId);
const startedTurnId = startedTurns.get(event.threadId);
const isTrackedTurn = sameId(startedTurnId, turnId);
if (isTrackedTurn) {
startedTurns.delete(event.threadId);
}
if (event.type === "turn.completed") {
yield* refreshLocalGitStatusAfterAgentCommand(event);
}
if (
turnId !== null &&
thread !== undefined &&
(isTrackedTurn ||
sameId(thread.session?.activeTurnId, turnId) ||
(startedTurnId === undefined && !thread.session?.activeTurnId))
) {
pendingTurns.delete(event.threadId);
yield* pullRequests.refreshAfterTurn;
}
if (event.type === "turn.aborted") {
return;
}
yield* captureCheckpointFromTurnCompletion(event).pipe(
Effect.catch((error) =>
Effect.flatMap(nowIso, (createdAt) =>
Expand Down Expand Up @@ -1012,6 +1055,8 @@ const make = Effect.gen(function* () {
event.type !== "turn.started" &&
event.type !== "session.configured" &&
event.type !== "turn.completed" &&
event.type !== "turn.aborted" &&
event.type !== "session.exited" &&
!(event.type === "item.completed" && event.payload.itemType === "command_execution")
) {
return Effect.void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ describe("OrchestrationEngine", () => {
Layer.provide(
Layer.succeed(ProjectionSnapshotQuery, {
getCommandReadModel: () => Effect.succeed(commandReadModel),
getUserInputActivity: () => Effect.die("unused"),
getSnapshot: () =>
Effect.sync(() => {
fullSnapshotReadCount += 1;
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/OrchestrationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,19 @@ const makeOrchestrationEngine = Effect.gen(function* () {
});
}

// Command snapshots intentionally omit activity bodies. Read this
// request's durable state before deciding whether an async answer is
// a provider response or a new user turn.
const userInputActivity =
envelope.command.type === "thread.user-input.respond"
? yield* projectionSnapshotQuery.getUserInputActivity(envelope.command)
: Option.none();
const eventBase = yield* decideOrchestrationCommand({
command: envelope.command,
readModel: commandReadModel,
...(Option.isSome(userInputActivity)
? { userInputActivity: userInputActivity.value }
: {}),
}).pipe(
Effect.provideService(Crypto.Crypto, crypto),
Effect.mapError((cause) =>
Expand Down
65 changes: 45 additions & 20 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,26 +435,13 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
return;
}

const [messages, proposedPlans, activities, pendingApprovals] = yield* Effect.all([
projectionThreadMessageRepository.listByThreadId({ threadId }),
projectionThreadProposedPlanRepository.listByThreadId({ threadId }),
projectionThreadActivityRepository.listByThreadId({ threadId }),
projectionPendingApprovalRepository.listByThreadId({ threadId }),
]);

let latestUserMessageAt: string | null = null;
for (const message of messages) {
if (
message.role === "user" &&
(latestUserMessageAt === null || message.createdAt > latestUserMessageAt)
) {
latestUserMessageAt = message.createdAt;
}
}

const pendingApprovalCount = pendingApprovals.filter(
(approval) => approval.status === "pending",
).length;
const [latestUserMessageAt, proposedPlans, activities, pendingApprovalCount] =
yield* Effect.all([
projectionThreadMessageRepository.getLatestUserMessageAt({ threadId }),
projectionThreadProposedPlanRepository.listByThreadId({ threadId }),
projectionThreadActivityRepository.listByThreadId({ threadId }),
projectionPendingApprovalRepository.countPendingByThreadId({ threadId }),
]);
const pendingUserInputCount = derivePendingUserInputCountFromActivities(activities);
const hasActionableProposedPlan = deriveHasActionableProposedPlan({
latestTurnId: existingRow.value.latestTurnId,
Expand Down Expand Up @@ -1059,6 +1046,21 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
return;

case "thread.turn-start-requested": {
const pendingTurnStart = yield* projectionTurnRepository.getPendingTurnStartByThreadId({
threadId: event.payload.threadId,
});
if (Option.isSome(pendingTurnStart)) {
const pendingMessage = yield* projectionThreadMessageRepository.getByMessageId({
messageId: pendingTurnStart.value.messageId,
});
if (
Option.isSome(pendingMessage) &&
pendingMessage.value.role === "user" &&
pendingMessage.value.text.trim().toLowerCase() === "/compact"
) {
return;
}
}
yield* projectionTurnRepository.replacePendingTurnStart({
threadId: event.payload.threadId,
messageId: event.payload.messageId,
Expand All @@ -1069,10 +1071,33 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
return;
}

case "thread.activity-appended": {
if (
event.payload.activity.kind !== "context-compaction" &&
event.payload.activity.kind !== "provider.turn.start.failed"
) {
return;
}
const pendingTurnStart = yield* projectionTurnRepository.getPendingTurnStartByThreadId(
event.payload,
);
if (
Option.isNone(pendingTurnStart) ||
String(pendingTurnStart.value.messageId) !==
extractActivityRequestId(event.payload.activity.payload)
) {
return;
}
yield* projectionTurnRepository.deletePendingTurnStartByThreadId(event.payload);
return;
}

case "thread.session-set": {
const turnId = event.payload.session.activeTurnId;
if (turnId === null || event.payload.session.status !== "running") {
if (
(event.payload.session.status === "ready" &&
event.commandId?.startsWith("server:provider-session-set:") === true) ||
event.payload.session.status === "error" ||
event.payload.session.status === "stopped" ||
event.payload.session.status === "interrupted"
Expand Down
Loading