Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
240 changes: 171 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,25 @@ 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;
}) {
/**
* Sessions whose auto-settle aborted and that have not been seen at rest
* since.
*
* Two earlier gates were wrong. A revision check never re-arms: a turn
* COMPLETING does not move the settle lifecycle, so the PR would be skipped
* forever. A time-based cooldown expires while a long turn is still running,
* so in step 3 the retry would stop the very work that won the race.
*
* "At rest" is the actual signal, and it is the same one the canonical settle
* tier uses: a session is eligible again once it is no longer running, or its
* runtime has gone idle. It re-arms exactly when the activity ends.
* Instance-scoped, so it lives as long as the poller.
*/
const abortedSessionIds = new Set<string>();

/**
* 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 +145,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 +194,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 +210,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",
));
// Wait for the activity that won the race to finish. The abort signal is
// edge-triggered, so a turn that is STILL running never re-trips it;
// retrying regardless would, in step 3, stop the very work that beat the
// first attempt. Gating on "at rest" defers the merge without ever
// abandoning it.
if (abortedSessionIds.has(session.id)) {
const current = args.sessionService.get(session.id);
const status = (current?.status ?? "").toLowerCase();
const runtime = (current?.runtimeState ?? "").toLowerCase();
const atRest = status !== "running" || runtime === "idle";

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 Use projected chat state to re-arm retries

For chat sessions, this gate never observes turn completion in production: sessionService.get() returns the raw persisted row and derives runtimeState directly from status, while chat rows intentionally keep status = "running" between turns; only chatSessionProjection.ts changes an idle chat to runtimeState = "idle". Thus, after a chat turn aborts auto-settlement, every later poll keeps the merged PR unfinished indefinitely. This is fresh evidence against the prior retry finding: the new test injects an idle runtime state that the directly wired production sessionService.get() cannot return for a persisted running chat.

Useful? React with 👍 / 👎.

if (!atRest) {
abandonedThisPr = true;
continue;
}
abortedSessionIds.delete(session.id);
}
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. Hold the retry
// until the session is seen at rest.
abandonedThisPr = true;
abortedSessionIds.add(session.id);
}
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
149 changes: 121 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,68 @@ 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") {
// Report it. A joiner that returns nothing looks identical to a settle
// that was never eligible, and a caller with a durable consequence — the
// PR poller marking a merge handled — would consume the merge on the
// strength of someone else's in-flight settle that may yet abort.
aborted.push({ sessionId: id, reason: "joined_in_flight" });
continue;
}
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 +1384,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 +1410,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 +1509,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 +1592,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 +1770,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 +1799,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 +1824,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