Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
228 changes: 159 additions & 69 deletions apps/desktop/src/main/services/prs/prAsync.test.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,26 @@ function resolveMergeSettlementScope(pr: PrSummary, snapshot: PrSummary[]): Merg

export function createPrMergeAutoSettlementService(args: {
db: Pick<AdeDb, "getJson" | "setJson">;
sessionService: Pick<ReturnType<typeof createSessionService>, "get" | "list" | "settleSessionsWithOutcome">;
sessionService: Pick<ReturnType<typeof createSessionService>, "get" | "list" | "settleSessionsReportingAborts">;
emitEvent: (event: PrEventPayload) => void;
}) {
/**
* When each session's auto-settle last aborted.
*
* A revision check was tried and is wrong: a normal turn COMPLETING does not
* move the settle lifecycle (`clearLastTurnFailed` does not touch it, and chat
* output deliberately opts out of clearing), so the revision can stay equal
* forever and the merged PR would be skipped for good — worse than the
* over-retry it was meant to fix.
*
* A cooling-off period is the honest gate: it always re-arms, so the PR is
* never permanently abandoned, while the poll interval stops re-running
* teardown against work that is still in progress. Instance-scoped, so it
* lives exactly as long as the poller.
*/
const lastAbortedAtBySession = new Map<string, number>();
const RETRY_AFTER_ABORT_MS = 10 * 60 * 1000;

/**
* The currently open or draft PRs in the previous snapshot, so a merge we
* WATCHED can be told apart from one that was already history when it arrived.
Expand Down Expand Up @@ -129,11 +146,19 @@ export function createPrMergeAutoSettlementService(args: {
// successful pass, so a snapshot that returns early does not silently
// consume its own evidence.
const previouslyWatchedPrIds = new Set(previouslyWatchablePrIds);
// Ids whose merge this pass watched but could not finish filing, because a
// session became active mid-settle. Kept watchable so the retry can still
// announce: `watchedItMerge` is what gates the toast, and a merged PR is
// otherwise dropped from the watchable set at the end of every pass — so
// without this the retry settles the session silently and the user never
// learns their PR merged.
const unfinishedMergePrIds = new Set<string>();
const rememberSnapshot = () => {
previouslyWatchablePrIds.clear();
for (const pr of prs) {
if (pr.state === "draft" || pr.state === "open") previouslyWatchablePrIds.add(pr.id);
}
for (const id of unfinishedMergePrIds) previouslyWatchablePrIds.add(id);
};
const settings = getSessionLifecycleSettings(args.db);
const state = getPrMergeAutoSettlementState(args.db);
Expand Down Expand Up @@ -170,6 +195,7 @@ export function createPrMergeAutoSettlementService(args: {
);

const settledSessionIds: string[] = [];
let abandonedThisPr = false;
for (const session of rows) {
const currentSettings = getSessionLifecycleSettings(args.db);
const currentState = getPrMergeAutoSettlementState(args.db);
Expand All @@ -185,23 +211,52 @@ export function createPrMergeAutoSettlementService(args: {
// session even when it still owns scheduled work, a background task,
// or another normal settlement blocker. Real activity can unsettle it
// again, while handledPrIds prevents this PR from filing it twice.
settledSessionIds.push(...args.sessionService.settleSessionsWithOutcome(
[session.id],
`PR #${pr.githubPrNumber} merged`,
polledAt,
"pr_merge",
));
// Cool off after an abort. The abort signal is edge-triggered — a turn
// that is STILL running will not trip it again — so retrying every poll
// would, in step 3, stop the very work that beat the first attempt, over
// and over. The window always expires, so the merge is deferred, never
// abandoned.
const lastAbortedAt = lastAbortedAtBySession.get(session.id);
const polledAtMs = Date.parse(polledAt);
if (
lastAbortedAt !== undefined
&& Number.isFinite(polledAtMs)
&& polledAtMs - lastAbortedAt < RETRY_AFTER_ABORT_MS
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep retries blocked while the turn remains active

Fresh evidence in this replacement gate is that the cooldown expires solely by elapsed time, independent of whether the turn that caused the abort has finished. For a chat or tracked-agent turn lasting over ten minutes, completion has not re-fired turn_start, and its output deliberately does not advance the lifecycle revision, so the next retry runs teardown against that same active work and then settles it. Re-arm on an inactive/completed-turn signal rather than unconditional timeout expiry.

Useful? React with 👍 / 👎.

abandonedThisPr = true;
continue;
}
const settleResult = args.sessionService.settleSessionsReportingAborts([session.id], {
outcome: `PR #${pr.githubPrNumber} merged`,
settledAt: polledAt,
source: "pr_merge",
});
settledSessionIds.push(...settleResult.settled);
if (settleResult.aborted.length) {
// The session became active while the settle was in flight. Leaving
// the PR unhandled is the point: a later pass retries, instead of this
// merge being consumed by a settle that never landed. Record the
// revision so that retry waits for the session to actually change.
abandonedThisPr = true;
const abortedAtMs = Date.parse(polledAt);
if (Number.isFinite(abortedAtMs)) lastAbortedAtBySession.set(session.id, abortedAtMs);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

const finalSettings = getSessionLifecycleSettings(args.db);
// An abandoned settle must not consume the merge. `handledPrIds` is the
// only thing that would stop a later pass from retrying, and the whole
// reason the outcome is typed is so this branch can exist.
if (abandonedThisPr) unfinishedMergePrIds.add(pr.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wait for activity to finish before retrying auto-settle

When a settle aborts because a turn started or attention was requested, retaining the PR here makes the next snapshot retry unconditionally. Candidate selection at lines 174-177 deliberately ignores active-turn/background-work blockers, while the abort signal is edge-triggered and will not fire again merely because that same turn is still running. Once step 3 attaches real teardown, the next poll can therefore stop the work that won the first race; retain the merge without retrying until the session is no longer active.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer the merge event until all session settles finish

When a PR has multiple candidate sessions and an early session settles but a later one aborts, this keeps the PR watched and unhandled while the nonempty settledSessionIds still causes an event later in the same pass. Once the aborted session becomes eligible, the retry emits another pr-sessions-auto-settled event for the same merge, producing duplicate desktop/mobile notifications with the count split across them. Defer notification until abandonedThisPr is false, preserving the successful IDs across retries if the final event should include the complete set.

Useful? React with 👍 / 👎.

const finalState = getPrMergeAutoSettlementState(args.db);
// Mark this PR handled even when its session had background work, and
// even when the scope came back `ambiguous` and nothing was filed at all:
// the merge itself is the explicit override, this one looked and decided,
// and a later user reactivation belongs to a new lifecycle rather than to
// this already-consumed merge.
if (
finalSettings.autoSettleLaneSessionsOnPrMerge
!abandonedThisPr
&& finalSettings.autoSettleLaneSessionsOnPrMerge
&& finalState?.enabledSince
&& !finalState.handledPrIds.includes(pr.id)
&& isMergeAtOrAfter(pr.mergedAt, finalState.enabledSince)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1356,12 +1356,10 @@ describe("sessionService resume metadata", () => {
expect(service.get("session-new")?.settledAt).not.toBeNull();
expect(service.get("session-other")?.settledAt).toBeNull();

expect(service.settleSessionsWithOutcome(
expect(service.settleSessionsReportingAborts(
["session-settled", "session-other"],
"PR #841 merged",
"2026-03-17T03:00:00.000Z",
"pr_merge",
)).toEqual(["session-other"]);
{ outcome: "PR #841 merged", settledAt: "2026-03-17T03:00:00.000Z", source: "pr_merge" },
).settled).toEqual(["session-other"]);
expect(service.get("session-settled")).toEqual(expect.objectContaining({
settledAt: "2026-03-17T01:00:00.000Z",
statusNote: null,
Expand Down
142 changes: 114 additions & 28 deletions apps/desktop/src/main/services/sessions/sessionService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs";
import type { AdeDb } from "../state/kvDb";
import { createSettleLifecycleWriter } from "./settleLifecycleWriter";
import type { SettleAbortedSession, SettleSessionsOutcome, SettleTeardownCompleted } from "./settlingStateRegistry";
import type {
ClaudeSessionPointer,
SessionAttentionSource,
Expand Down Expand Up @@ -365,7 +366,21 @@ function normalizeSessionIds(sessionIds: string[]): string[] {
));
}

export function createSessionService({ db }: { db: AdeDb }) {
export function createSessionService({
db,
runSettleTeardown,
}: {
db: AdeDb;
/**
* Stop the session's background work. Injected rather than per-call: teardown
* is a service capability, not something a caller decides.
*
* Step 2 ships with this absent — the settling window, the abort rule, and the
* revision guard all land and are tested against a NO-OP, so every race is
* exercised before there is any work to lose. Step 3 supplies the real one.
*/
runSettleTeardown?: (sessionId: string) => SettleTeardownCompleted;
}) {
const changeListeners = new Set<(event: TerminalSessionChangedEvent) => void>();


Expand Down Expand Up @@ -718,6 +733,61 @@ export function createSessionService({ db }: { db: AdeDb }) {
return newlySettled;
};

/**
* Settle through the settling window: the shape a real teardown will run in.
*
* Per session: read the revision, open the window, run teardown, then apply
* the settle ONLY if nothing moved. "Nothing moved" is two checks that catch
* different things — the abort flag (a human decision arrived and said so) and
* the revision (anything else changed the settle tuple, including a change
* this host did not make through a caller).
*
* Teardown is a NO-OP in step 2 by design. The point of landing the window
* first is that every race is testable before there is any work to lose.
*/
const settleManyWithTeardown = (
sessionIds: string[],
options: { outcome?: string; settledAt?: string; source?: SessionSettleSource } = {},
): SettleSessionsOutcome => {
const ids = normalizeSessionIds(sessionIds);
const settled: string[] = [];
const aborted: SettleAbortedSession[] = [];

for (const id of ids) {
const revisionBefore = settleLifecycle.readRevision(id);
const begin = settleLifecycle.settling.begin(id, revisionBefore);
// Joined an in-flight settle rather than starting a second teardown: R4.
// The owner will report the outcome; reporting it twice would double-count.
if (begin.kind === "joined") continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Return the owner outcome to joined settle callers

When PR auto-settlement joins a user or operator settle already in flight, this returns { settled: [], aborted: [] } immediately even though that caller has its own durable consequence. The PR service therefore treats the merge as successfully processed and marks it handled; if the owning settle subsequently aborts, the PR is never retried, and even on success the PR-specific outcome and notification are lost. Joined callers need to receive the eventual owning result rather than an unaccounted empty outcome.

Useful? React with 👍 / 👎.

try {
runSettleTeardown?.(id);

const abortedBy = settleLifecycle.settling.abortedBy(id);
if (abortedBy) {
aborted.push({ sessionId: id, reason: abortedBy });
continue;
}
// The revision catches everything the abort flag cannot: a settle-tuple
// change from a path that never announced itself as a decision.
if (settleLifecycle.readRevision(id) !== revisionBefore) {
aborted.push({ sessionId: id, reason: "lifecycle_changed" });
continue;
}
settled.push(...settleMany([id], options));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve peer reactivation during the settle apply

When a sibling ADE process or paired desktop applies an unsettle or keep-active override after the local revision check, that replicated tuple write bypasses this host's lifecycle revision, and this subsequent settleMany can overwrite the peer's explicit decision and settle the session anyway. The added R7 test exercises only the benign peer-settle case and explicitly leaves peer reactivation unresolved; the apply must account for the current tuple or a peer-visible concurrency token before landing the settle.

Useful? React with 👍 / 👎.

} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let persistence failures escape the teardown catch

When settleMany([id], options) fails because SQLite cannot read or write the session row—for example during a lock timeout or I/O failure—this broad catch misreports the failure as teardown_failed. Callers then cannot distinguish a successful teardown followed by failed persistence: settleSessions collapses it to an empty changed-ID list, settleSession still returns true, and PR auto-settlement may retry teardown unnecessarily. Limit this catch to runSettleTeardown and allow persistence failures to propagate through the existing error path.

Useful? React with 👍 / 👎.

// A throw must not discard the accounting for the rest of the batch, nor
// leave earlier sessions settled with no record of it. Report this id and
// carry on: a bulk caller always gets a full accounting.
aborted.push({ sessionId: id, reason: "teardown_failed" });
void error;
} finally {
settleLifecycle.settling.end(id);
}
}

return { settled, aborted };
};

return {
list,

Expand Down Expand Up @@ -1307,7 +1377,7 @@ export function createSessionService({ db }: { db: AdeDb }) {
return;
}
writeSettleLifecycle({
intent: { kind: "clearOnActivity" },
intent: { kind: "clearOnActivity", cause: "mechanical" },
extraSet: { last_output_preview: preview, last_output_at: now },
sessionIds: [sessionId],
});
Expand All @@ -1333,7 +1403,7 @@ export function createSessionService({ db }: { db: AdeDb }) {
return;
}
writeSettleLifecycle({
intent: { kind: "clearOnActivity" },
intent: { kind: "clearOnActivity", cause: "mechanical" },
extraSet: { last_output_at: at },
sessionIds: [sessionId],
});
Expand Down Expand Up @@ -1432,22 +1502,26 @@ export function createSessionService({ db }: { db: AdeDb }) {
sessionId: string,
opts: { outcome?: string | null; settledAt?: string; source?: SessionSettleSource } = {},
): boolean {
const settledAt = normalizeIsoTimestamp(opts.settledAt) ?? new Date().toISOString();
const outcome = normalizeSessionStatusNote(opts.outcome);
return mutateSessionMeta(sessionId, (id) => {
// An explicit settle also drops a stale keep-active pin — otherwise the
// override would silently veto the settle the user just asked for.
writeSettleLifecycle({
intent: { kind: "settle", settledAt, source: opts.source ?? "user" },
extraSet: {
...(outcome ? { status_note: outcome } : {}),
attention_requested_at: null,
attention_message: null,
attention_source: null,
},
sessionIds: [id],
});
// Through the settling window, like the bulk paths. This is the route a
// USER takes (row menu -> settleTerminalSession -> here), which is the
// "user settle" R4 names — so it has to be joinable and abortable, and in
// step 3 it has to run teardown. Routing it here is what makes the R4
// claim true rather than only true of bulk callers.
const trimmed = sessionId.trim();
if (!trimmed) return false;
// `settleMany` returns [] for both "missing" and "already settled", so the
// boolean contract needs its own existence check to stay honest.
const exists = db.get<{ present: number }>(
"select 1 as present from terminal_sessions where id = ? limit 1",
[trimmed],
);
if (!exists) return false;
settleManyWithTeardown([trimmed], {
outcome: normalizeSessionStatusNote(opts.outcome) ?? undefined,
settledAt: opts.settledAt,
source: opts.source,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve status notes when no outcome is supplied

For settleSession(id) or a blank outcome, this object still owns an outcome property whose value is undefined. settleMany tests hasOwnProperty("outcome"), so settling an active row writes status_note = null; previously the single-session path omitted that column unless a normalized nonempty outcome existed. This erases an existing result note when a user re-settles after activity without supplying replacement text.

Useful? React with 👍 / 👎.

});
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate abort outcomes from single-session settles

When the teardown callback triggers clearTurnStartMarkers, requests attention, or fails, settleManyWithTeardown returns an aborted outcome, but this wrapper discards it and returns true solely because the row exists. Consequently settleTerminalSession, the action registry, and the CTO tool still report success even though the session was not settled, recreating the silent-success contract the typed outcome is intended to remove. Return or propagate the additive typed outcome through those interfaces instead of collapsing it to existence.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
},

/** Clears a declared settle plus any `'settled'` override. */
Expand Down Expand Up @@ -1511,16 +1585,28 @@ export function createSessionService({ db }: { db: AdeDb }) {
},

settleSessions(sessionIds: string[]): string[] {
return settleMany(sessionIds);
return settleManyWithTeardown(sessionIds).settled;
},

settleSessionsWithOutcome(
/**
* Settle, reporting abandoned sessions explicitly.
*
* `settleSessions` leaves an aborted id simply absent from its changed-id
* list, which is *almost* the right contract — a caller cannot tell "filed"
* from "not filed, and here is why". This is that distinction, and it is
* what a caller with a durable consequence (the PR-merge auto-settle marking
* a PR handled) has to branch on.
*/
settleSessionsReportingAborts(
sessionIds: string[],
outcome: string,
settledAt: string = new Date().toISOString(),
source: SessionSettleSource = "user",
): string[] {
return settleMany(sessionIds, { outcome, settledAt, source });
options: { outcome?: string; settledAt?: string; source?: SessionSettleSource } = {},
): SettleSessionsOutcome {
return settleManyWithTeardown(sessionIds, options);
},

/** Sessions currently mid-settle, for the visible `Settling…` state. */
settlingSessionIds(): string[] {
return settleLifecycle.settling.settlingSessionIds();
Comment on lines +1659 to +1661

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find IPC, preload, shared-type, and renderer references to the settling projection.
rg -nP -C3 '\bsettlingSessionIds\b|\bisSettling\b|Settling…' apps/desktop/src

Repository: arul28/ADE

Length of output: 6057


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate desktop files ---'
git ls-files apps/desktop/src | rg '(^|/)(ipc|preload|renderer|session|sessions|.*Service|.*service)' | head -200
printf '%s\n' '--- session service structure ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline apps/desktop/src/main/services/sessions/sessionService.ts
else
  rg -n 'export|settlingSessionIds|sessionService|ipcMain|handle|sessions' apps/desktop/src/main/services/sessions/sessionService.ts
fi
printf '%s\n' '--- session service references outside tests ---'
rg -n -P -C3 '\bsessionService\b|\bsettleSessionsReportingAborts\b|\bsettlingSessionIds\b' apps/desktop/src --glob '!**/*.test.ts' --glob '!**/*.spec.ts'
printf '%s\n' '--- IPC/preload session references ---'
rg -n -P -C3 'session|settle|Settling|isSettling' apps/desktop/src/main apps/desktop/src/preload apps/desktop/src/renderer --glob '*.{ts,tsx}' | head -400

Repository: arul28/ADE

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- exact settling references in all tracked desktop source ---'
rg -n -P -C2 '\bsettlingSessionIds\b|\bisSettling\b|Settling…' apps/desktop/src --glob '*.{ts,tsx}' || true
printf '%s\n' '--- desktop source top-level directories ---'
find apps/desktop/src -mindepth 1 -maxdepth 2 -type d | sort | head -100
printf '%s\n' '--- IPC registration and preload bridge files ---'
git ls-files apps/desktop/src | rg '(^|/)(ipc|preload|bridge|api|context|channels?)(/|\.|$)' | head -200
printf '%s\n' '--- renderer access to session APIs and status fields ---'
rg -n -P -C2 'window\.(api|electron|desktop)|sessions?\.(list|get|subscribe|on)|terminal\.(list|get|subscribe)|settledAt|settleOverride' apps/desktop/src/renderer apps/desktop/src/preload apps/desktop/src --glob '*.{ts,tsx}' | head -300

Repository: arul28/ADE

Length of output: 33536


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- session IPC registration ---'
rg -n -P -C8 'session\.list|sessions\.list|sessionService\.list|TerminalSessionSummary|settledAt|settleOverride' apps/desktop/src/main/services/ipc/registerIpc.ts apps/desktop/src/shared apps/desktop/src/preload/preload.ts apps/desktop/src/preload/global.d.ts --glob '*.{ts,tsx}' | head -500
printf '%s\n' '--- session bridge definitions ---'
rg -n -P -C6 'sessions\s*[:=]|list\s*:\s*.*session|onChanged|getDelta|readTranscriptTail' apps/desktop/src/preload/preload.ts apps/desktop/src/preload/global.d.ts apps/desktop/src/shared/ipc.ts
printf '%s\n' '--- session list implementation and summary construction ---'
rg -n -P -C8 'list\(|enrichSessions|TerminalSessionSummary|settledAt|settleOverride' apps/desktop/src/main/services/sessions apps/desktop/src/main/services/pty apps/desktop/src/shared/types --glob '*.{ts,tsx}' | head -500

Repository: arul28/ADE

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- exact summary type ---'
rg -n -C35 '^export (type|interface) TerminalSessionSummary|^export type TerminalSessionDetail' apps/desktop/src/shared/types/sessions.ts
printf '%s\n' '--- list projection implementation ---'
rg -n -P -C25 'listSessionsWithChatProjection|function listSessionsWithChatProjection|const listSessionsWithChatProjection' apps/desktop/src/main/services/ipc/registerIpc.ts
printf '%s\n' '--- all settling identifiers outside main session internals ---'
rg -n -P '\bsettlingSessionIds\b|\bisSettling\b|Settling…' apps/desktop/src/preload apps/desktop/src/shared apps/desktop/src/renderer || true

Repository: arul28/ADE

Length of output: 11137


Expose the Settling… state through the session IPC contract.

settlingSessionIds() is not included in TerminalSessionSummary, and no preload or renderer consumer exists. If this PR intends to render Settling…, update the shared type, IPC, preload bridge, renderer state, and change notifications. Otherwise, defer this projection explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/sessions/sessionService.ts` around lines 1597
- 1599, Expose settlingSessionIds() consistently across the session IPC
contract: add it to TerminalSessionSummary, wire it through the session IPC
handler and preload bridge, consume it in renderer state, and include it in
change notifications so the Settling… UI stays current. If this projection is
not intended in this PR, remove or explicitly defer the settlingSessionIds()
projection instead.

Source: Path instructions

},

unsettleSessions(sessionIds: string[]): void {
Expand Down Expand Up @@ -1677,7 +1763,7 @@ export function createSessionService({ db }: { db: AdeDb }) {
): boolean {
return mutateSessionMeta(sessionId, (id) => {
writeSettleLifecycle({
intent: { kind: "clearOnActivity" },
intent: { kind: "clearOnActivity", cause: "attention_requested" },
extraSet: {
attention_requested_at: new Date().toISOString(),
attention_message: normalizeOptionalText(message, 500),
Expand Down Expand Up @@ -1706,7 +1792,7 @@ export function createSessionService({ db }: { db: AdeDb }) {
// settled/failed mutually exclusive at write time, so every surface's
// precedence order agrees by construction.
writeSettleLifecycle({
intent: { kind: "clearOnActivity" },
intent: { kind: "clearOnActivity", cause: "turn_failed" },
extraSet: { last_turn_failed_at: failedAt },
sessionIds: [id],
});
Expand All @@ -1731,7 +1817,7 @@ export function createSessionService({ db }: { db: AdeDb }) {
clearTurnStartMarkers(sessionId: string): boolean {
const changed = mutateSessionMeta(sessionId, (id) => {
writeSettleLifecycle({
intent: { kind: "clearOnActivity" },
intent: { kind: "clearOnActivity", cause: "turn_start" },
extraSet: {
last_turn_failed_at: null,
attention_requested_at: null,
Expand Down
Loading
Loading