You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by "me2seeks (via GitHub)" <gi...@apache.org> on 2026/10/01 11:33:41 UTC

[PR] feat(desktop): offer turn resume from the composer send slot [maka]

me2seeks opened a new pull request, #5904:
URL: https://github.com/apache/maka/pull/5904

   ## Summary
   
   After an interrupted turn (app restart, provider timeout, manual Stop), resume was only reachable from the interrupted turn's transcript banner — the composer send slot always started a new turn, and the renderer had no authoritative way to offer resume anywhere else (the banner gates on a local heuristic over the last turn's surface state).
   
   This PR lets the composer offer resume from the send slot. Desktop main already called the Runtime Host's read-only `turn.resume.query` planner inside `sessions:resumeLatest`; the query is now exposed to the renderer on its own as `sessions:queryResumeLatest`. `useShellResume` tracks the plan per session — refreshing on session switch and on the active session's turn/status boundaries, the only moments the answer can change — and when the plan is `ready`, the composer's send slot shows **Resume** instead of Send while the draft is empty. Typing or staging context brings Send back; Stop always outranks it. The click reuses the existing `resumeLatest` handler, so the full safe-boundary admission (workspace identity, high-water, tool catalog) re-validates at click time and a stale offer can only park, never resume the wrong thing. The transcript banner is unchanged.
   
   Empty draft with a resumable interrupted turn — Resume owns the send slot:
   
   ![Resume owns the send slot while the draft is empty](./resume-offered.png)
   
   Typing anything switches the same slot back to Send:
   
   ![Typing restores the Send button](./typing-restores-send.png)
   
   The click in flight through the safe-boundary admission:
   
   ![Resume pending state](./resume-pending.png)
   
   Fixes #5903
   
   ## Verification
   
   - `npm run build`, `npm run typecheck`, `npm run lint`, `npm run format:check`: pass.
   - `npx knip --workspace apps/desktop`, `npx knip --workspace packages/ui`: pass.
   - New tests: `resume-availability.test.ts` (tracker: ready/parked/reject, stale-answer drop, burst coalescing, per-session keying), two `sessions:queryResumeLatest` IPC passthrough tests, send-policy `resumeShown` gates and send-slot render tests in `composer-send-toggle.test.tsx`.
   - `node --test` on the touched desktop and UI suites: 59 + 6 + 17 pass. Full `@maka/desktop` `test:dist`: 3118 pass, 6 fail — all 6 reproduce identically on a clean `origin/main` checkout of this machine (git-environment fixtures and one source pin; the source pin initially caught this change's `sharedSessionActive` mention, which was reworked so the Guest composer keeps its single sessionId projection, and now passes).
   - Screenshots above are from the new `composer-resume.stories.tsx` storybook stories (ResumeOffered / TypingRestoresSend / ResumePending), which also assert the slot swap in their play functions.
   - Not run: the Electron app end-to-end against a live interrupted session.
   
   ## AI use
   
   - [x] Generative tooling made a substantive contribution
   
   Tool(s) and scope: Maka (k3-256k) — issue analysis, implementation, tests, and this PR description under human direction. `Generated-by` trailer on the commit.
   
   ## Checklist
   
   - [x] Tests cover the change and fail without it
   - [x] Lint, format, typecheck and the affected suites pass locally
   
   Does this PR entail a change in behavior?
   
   - [x] Yes — described under Summary above
   
   ![Resume owns the send slot while the draft is empty](https://github.com/user-attachments/assets/49810640-8d98-4ef4-b677-80901cd8c23c)
   
   ![Typing restores the Send button](https://github.com/user-attachments/assets/54b3dce4-b488-42fc-9b52-d627676f24b4)
   
   ![Resume pending state](https://github.com/user-attachments/assets/13f4dd84-4bee-4c42-acef-bc58a4c2f097)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(desktop): offer turn resume from the composer send slot [maka]

Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #5904:
URL: https://github.com/apache/maka/pull/5904#discussion_r4157202944


##########
packages/ui/src/composer.tsx:
##########
@@ -2601,6 +2615,21 @@ export const Composer = forwardRef<
               }}
               icon={<Square size={ICON_SIZE.control} aria-hidden="true" />}
             />
+          ) : resumeShown && props.resumeAction ? (

Review Comment:
   **P2: Resume takes over the exact slot Stop occupied, under the user's cursor.**
   
   The planner answers `ready` for a turn aborted by `renderer.stop_button`. The PR lists manual Stop as a target, and `latestInterruptedResumeTurnId` treats it as resumable. The sequence is:
   1. The user clicks Stop. The button is disabled while `stopPending`.
   2. The turn ends, and the slot becomes a disabled Send.
   3. The query returns `ready`, and the slot becomes a primary **Resume** in the same place.
   
   A repeated or late click on Stop, which is common when stopping takes a moment, now restarts the turn the user just stopped, along with its tool calls and spend.
   
   **Possible fixes (any one):**
   - Don't offer the slot Resume when the latest turn was user-stopped; leave that case to the banner.
   - Arm the button only after a short delay, or after the pointer leaves the slot, following a Stop→non-Stop transition.
   - Ignore clicks that arrive within N ms of `stopShown` flipping to false.
   
   Please add a send-policy or render test for this transition.



##########
apps/desktop/src/renderer/features/conversation/controller/use-shell-resume.ts:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useRef, useState } from 'react';
+import type { UiLocale } from '@maka/core/ui-locale';
+import { resumeParkToastCopy } from '@maka/ui';
+import { localizedShellErrorMessage, type getShellCopy } from '../../../locales/shell-copy.js';
+import { createResumeAvailabilityTracker } from '../../../application/contracts/resume-availability.js';
+import { useConversationServices } from '../services.js';
+
+type ToastApi = {
+  info(title: string, description?: string): void;
+  error(
+    title: string,
+    description?: string,
+    diagnosticDetails?: string,
+    diagnosticTarget?: { sessionId: string },
+  ): void;
+};
+
+/**
+ * Owns the #1223 safe-boundary resume cluster — the in-flight
+ * `resumePendingSessionId` guard, the per-session parked-diagnostic
+ * descriptions, and the `resumeInterruptedSession` handler driving
+ * `sessions.resumeLatest` — and, since #5903, `resumeAvailableBySession`: the
+ * authoritative answer to "could the latest interrupted Turn resume right
+ * now", read from the Host's resume planner (`sessions.queryResumeLatest`)
+ * rather than the renderer's banner heuristic. Availability refreshes on
+ * session switch and on the tracked session's turn/status boundaries — the
+ * only moments the answer can change — and on the click's own outcome.
+ *
+ * Both consumers get a fully built action so the call sites stay one-line
+ * pass-throughs: `safeResumeAction` for the interrupted-Turn banner,
+ * `composerResumeAction` for the composer's send-slot offer. Building them
+ * here is also what keeps one hook instance behind both — the pending guard
+ * is shared, so the banner and the composer button can never race a second
+ * resume IPC past the first.
+ *
+ * `activeId` is the displayed session (what the pending/detail/gating reads
+ * key on); `ownerActiveId` is what the handler snapshots and what
+ * availability tracks, so a session switch mid-resume settles the ORIGINAL
+ * session's flags. The Guest fork of the composer props never picks
+ * `resumeAction` up (guestComposerProps chooses its keys), so the composer
+ * offer needs no shared-session gate of its own.
+ */
+export function useShellResume(options: {
+  activeId: string | undefined;
+  ownerActiveId: string | undefined;
+  sharedSessionActive: boolean;
+  toastApi: ToastApi;
+  shellCopy: ReturnType<typeof getShellCopy>['app'];
+  uiLocale: UiLocale;
+}): {
+  safeResumeAction: { pending: boolean; detail: string | undefined; onResume(): void } | undefined;
+  composerResumeAction: { pending: boolean; onResume(): void } | undefined;
+} {
+  const { activeId, ownerActiveId, sharedSessionActive, toastApi, shellCopy, uiLocale } = options;
+  const services = useConversationServices();
+  const [resumePendingSessionId, setResumePendingSessionId] = useState<string | null>(null);
+  const [resumeParkDescriptionBySession, setResumeParkDescriptionBySession] = useState<Record<string, string>>({});
+  const [resumeAvailableBySession, setResumeAvailableBySession] = useState<Record<string, boolean>>({});
+  const ownerActiveIdRef = useRef(ownerActiveId);
+  ownerActiveIdRef.current = ownerActiveId;
+  const trackerRef = useRef<ReturnType<typeof createResumeAvailabilityTracker> | null>(null);
+  if (trackerRef.current === null) {
+    trackerRef.current = createResumeAvailabilityTracker({
+      query: (sessionId) => services.resume.queryPlan(sessionId),
+      onAvailability: (sessionId, available) => {
+        setResumeAvailableBySession((current) =>
+          current[sessionId] === available ? current : { ...current, [sessionId]: available },
+        );
+      },
+    });
+  }
+  const tracker = trackerRef.current;
+
+  // A session switch re-reads the offer for the session being opened; a stale
+  // answer for the session left behind is keyed by id and cannot bleed across.
+  useEffect(() => {
+    if (ownerActiveId === undefined) return;
+    tracker.request(ownerActiveId);

Review Comment:
   **P3: availability is cached per session and goes stale.**
   
   When `ownerActiveId` changes, this effect issues a new read but leaves `resumeAvailableBySession[ownerActiveId]` at whatever it was when the user last viewed that session. Until the read lands, an old `true` still renders Resume. Afterwards, only `turn-status-change` and `status-change` refresh it (L107). A Host `rebound`, a resume-feature toggle, or a resume from another client while the user is elsewhere is not picked up. Clicking a stale offer parks and shows a red error toast for an action the UI itself offered.
   
   **Fix:** mark the session as unknown (`false`) when the effect issues the request, so the offer is fail-closed until the fresh answer arrives. Also consider adding `rebound` to the reasons on L107.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(desktop): offer turn resume from the composer send slot [maka]

Posted by "me2seeks (via GitHub)" <gi...@apache.org>.
me2seeks commented on PR #5904:
URL: https://github.com/apache/maka/pull/5904#issuecomment-5937331807

   Review addressed in d14a5566:
   
   - **P2 (Resume takes over the slot Stop occupied)**: the send slot no longer offers the Turn the user just stopped — the composer's stop paths note the stop and the availability tracker suppresses exactly the offer that stop produces. Mechanism and tests in the inline reply.
   - **P3 (stale availability)**: a fresh read now retracts the visible offer until the new answer lands (fail-closed while stale), and `rebound` triggers a refresh. Details in the inline reply.
   - **Not verified (Host window)**: already pinned at the composition level by `production composition enables an explicit resume after user Stop by default` (`packages/runtime-host/src/__tests__/execution-composition.test.ts:1498`), which starts a real Turn, stops it via `turn.stop`, waits for the aborted terminal, and asserts `turn.resume.query` answers `ready`. If the renderer's re-query lands in the `observeExecutionCompletion` → `#executions` release window it gets `session_busy`, which the tracker maps to not-offered (fail-closed) until the following `status-change` re-read — that re-read path is what the tracker tests cover.
   - The Resume button icon is now the media-player Play triangle instead of RotateCcw, per maintainer feedback.
   
   Local verification: resume-availability suite 10/10, desktop dist suite 3124/3129 (the 5 failures are the known environment-specific git-review/project-management/skills ones that reproduce without this PR), @maka/ui 700/700, all four typechecks, renderer-architecture `--strict-base`, app-shell-hooks, and biome lint pass. Storybook smoke passes for all three resume stories; the one local failure (`product-settings-pages--permission-center-diagnostics-expanded`) is a settings story this PR does not touch and it reproduces on this machine independently.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(desktop): offer turn resume from the composer send slot [maka]

Posted by "me2seeks (via GitHub)" <gi...@apache.org>.
me2seeks commented on code in PR #5904:
URL: https://github.com/apache/maka/pull/5904#discussion_r4158692760


##########
packages/ui/src/composer.tsx:
##########
@@ -2601,6 +2615,21 @@ export const Composer = forwardRef<
               }}
               icon={<Square size={ICON_SIZE.control} aria-hidden="true" />}
             />
+          ) : resumeShown && props.resumeAction ? (

Review Comment:
   Fixed in d14a5566 — took the first option's semantics, applied before the offer ever reaches the send slot.
   
   The composer's stop paths (the Stop button and Escape, both gated on `streaming`) now report the stop through `useShellResume`'s new `noteUserStoppedTurn`, and the availability tracker suppresses exactly the first `ready` answer that follows. That suppression is exact rather than timed: while a Turn runs, the planner only answers `session_busy`, so the first `ready` after a user stop is the Turn the user just stopped. It never renders in the slot Stop occupied, so a repeated or late click lands on a disabled Send. The interrupted-Turn banner remains the deliberate resume path for that Turn; the next interruption the user did not cause offers normally again (the suppression consumes exactly one `ready` answer, per session).
   
   On the requested test: the suppression happens inside the tracker, upstream of `deriveComposerSendPolicy` — by the time the policy runs there is no `resumeOffered` for that Turn, so a send-policy or render test cannot observe the transition. The transition is pinned by tracker tests instead:
   - `suppresses the first offer after the user stops a Turn, exactly once`
   - `hides a visible offer the moment the user stops a Turn`
   - `keeps the stop suppression until a ready answer consumes it` (parked `session_busy` answers cannot spend it)
   - `scopes the stop suppression to the session that was stopped`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(desktop): offer turn resume from the composer send slot [maka]

Posted by "me2seeks (via GitHub)" <gi...@apache.org>.
me2seeks merged PR #5904:
URL: https://github.com/apache/maka/pull/5904


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(desktop): offer turn resume from the composer send slot [maka]

Posted by "jackwener (via GitHub)" <gi...@apache.org>.
jackwener commented on code in PR #5904:
URL: https://github.com/apache/maka/pull/5904#discussion_r4163289900


##########
apps/desktop/src/renderer/application/contracts/resume-availability.ts:
##########
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Authoritative resume availability behind the composer's Resume offer
+ * (#5903). The renderer never derives resumability from local turn state —
+ * the Runtime Host's safe-boundary planner is the only authority, and the
+ * read-only `turn.resume.query` preview it answers is side-effect free.
+ *
+ * The tracker owns the round trips behind one rule: the latest request for a
+ * session wins. A stale answer (an event burst, a session switch mid-flight)
+ * is dropped rather than applied out of order; a parked plan or a failed read
+ * both mean "not offered", keeping the affordance fail-closed. Clicking still
+ * re-runs the full admission inside `turn.resume.start`, so a stale offer can
+ * only ever park, never resume the wrong thing.
+ *
+ * Two fail-closed guards extend that rule:
+ *
+ * - A fresh read retracts the previous offer while it is in flight. The
+ *   cached answer went stale the moment the new question was asked (a
+ *   session switch, a rebound, a status boundary), so the offer hides until
+ *   the authority answers again instead of rendering a clickable stale
+ *   offer whose click can only park (#5904).
+ *
+ * - `noteUserStopped` suppresses the first offer that appears after the
+ *   user stops a Turn. The composer's send slot is where the Stop button
+ *   just sat: offering Resume there a moment later means a repeated or
+ *   late click on Stop restarts the very Turn the user stopped, tool calls
+ *   and spend included. The interrupted-Turn banner remains the deliberate
+ *   resume path for that Turn; the slot offers it only for interruptions
+ *   the user did not just cause. The suppression is exact, not timed: while
+ *   a Turn runs the planner only ever answers `session_busy`, so the first
+ *   `ready` answer after a user stop is that stopped Turn, and it is
+ *   suppressed exactly once.
+ */
+export function createResumeAvailabilityTracker(options: {
+  query(sessionId: string): Promise<{ readonly disposition: 'ready' | 'parked' }>;
+  onAvailability(sessionId: string, available: boolean): void;
+}) {
+  /** Per-session sequence of the newest request; answers older than it are stale. */
+  const latestRequestBySession = new Map<string, number>();
+  /** Per-session last emitted offer, so a re-read retracts only a visible one. */
+  const lastEmittedBySession = new Map<string, boolean>();
+  /** Sessions whose next ready answer is the Turn the user just stopped. */
+  const offerSuppressedAfterUserStop = new Set<string>();
+  const inFlight = new Set<string>();
+  const rerequestWhenSettled = new Set<string>();
+  let sequence = 0;
+
+  function emit(sessionId: string, available: boolean): void {
+    lastEmittedBySession.set(sessionId, available);
+    options.onAvailability(sessionId, available);
+  }
+
+  async function run(sessionId: string, requestId: number): Promise<void> {
+    try {
+      const plan = await options.query(sessionId);
+      if (latestRequestBySession.get(sessionId) !== requestId) return;
+      if (plan.disposition === 'ready' && offerSuppressedAfterUserStop.delete(sessionId)) {

Review Comment:
   [P2] Keep Stop suppression bound to the stopped resume candidate
   
   Deleting the session from this set consumes protection on the first **read**, even when the next read returns the very same stopped Run. One stop has two notification producers: `RuntimeHostSessionObserver.#acceptFrame` emits `turn-status-change` for the terminal frame (`runtime-host-session-observer.ts:851`), and `createRuntimeHostStopSession` emits it again after `interruptTurn` resolves (`runtime-host-session-execution-ipc-main.ts:986`). If these cause an in-flight query plus a trailing query, both can return ready for the stopped candidate. The first deletes suppression here; the second reaches `emit(..., true)` on line 79 and restores Resume where Stop was.
   
   I reproduced this with the production hook and mounted Composer: note Stop for A, deliver two terminal notifications, then resolve both queries with identical `sourceRunId`, `sourceTurnId`, and high-water. The first hides Resume; the second shows it. A subsequent click invokes `services.resume.start(A)` (`use-shell-resume.ts:129`), so a repeated Stop click can request continuation of the Turn just stopped. Click-time validation does not reject a legitimately resumable candidate merely because the user stopped it.
   
   Retain suppression by candidate identity across repeated reads, and release it only on an appropriate candidate/lifecycle change. The tracker currently narrows the plan to disposition alone, which cannot distinguish another observation of this Turn from a later interruption. Add a regression with two ready answers for the **same** source Run, not just two unlabeled ready answers assumed to represent different interruptions.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(desktop): offer turn resume from the composer send slot [maka]

Posted by "me2seeks (via GitHub)" <gi...@apache.org>.
me2seeks commented on code in PR #5904:
URL: https://github.com/apache/maka/pull/5904#discussion_r4158694917


##########
apps/desktop/src/renderer/features/conversation/controller/use-shell-resume.ts:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useRef, useState } from 'react';
+import type { UiLocale } from '@maka/core/ui-locale';
+import { resumeParkToastCopy } from '@maka/ui';
+import { localizedShellErrorMessage, type getShellCopy } from '../../../locales/shell-copy.js';
+import { createResumeAvailabilityTracker } from '../../../application/contracts/resume-availability.js';
+import { useConversationServices } from '../services.js';
+
+type ToastApi = {
+  info(title: string, description?: string): void;
+  error(
+    title: string,
+    description?: string,
+    diagnosticDetails?: string,
+    diagnosticTarget?: { sessionId: string },
+  ): void;
+};
+
+/**
+ * Owns the #1223 safe-boundary resume cluster — the in-flight
+ * `resumePendingSessionId` guard, the per-session parked-diagnostic
+ * descriptions, and the `resumeInterruptedSession` handler driving
+ * `sessions.resumeLatest` — and, since #5903, `resumeAvailableBySession`: the
+ * authoritative answer to "could the latest interrupted Turn resume right
+ * now", read from the Host's resume planner (`sessions.queryResumeLatest`)
+ * rather than the renderer's banner heuristic. Availability refreshes on
+ * session switch and on the tracked session's turn/status boundaries — the
+ * only moments the answer can change — and on the click's own outcome.
+ *
+ * Both consumers get a fully built action so the call sites stay one-line
+ * pass-throughs: `safeResumeAction` for the interrupted-Turn banner,
+ * `composerResumeAction` for the composer's send-slot offer. Building them
+ * here is also what keeps one hook instance behind both — the pending guard
+ * is shared, so the banner and the composer button can never race a second
+ * resume IPC past the first.
+ *
+ * `activeId` is the displayed session (what the pending/detail/gating reads
+ * key on); `ownerActiveId` is what the handler snapshots and what
+ * availability tracks, so a session switch mid-resume settles the ORIGINAL
+ * session's flags. The Guest fork of the composer props never picks
+ * `resumeAction` up (guestComposerProps chooses its keys), so the composer
+ * offer needs no shared-session gate of its own.
+ */
+export function useShellResume(options: {
+  activeId: string | undefined;
+  ownerActiveId: string | undefined;
+  sharedSessionActive: boolean;
+  toastApi: ToastApi;
+  shellCopy: ReturnType<typeof getShellCopy>['app'];
+  uiLocale: UiLocale;
+}): {
+  safeResumeAction: { pending: boolean; detail: string | undefined; onResume(): void } | undefined;
+  composerResumeAction: { pending: boolean; onResume(): void } | undefined;
+} {
+  const { activeId, ownerActiveId, sharedSessionActive, toastApi, shellCopy, uiLocale } = options;
+  const services = useConversationServices();
+  const [resumePendingSessionId, setResumePendingSessionId] = useState<string | null>(null);
+  const [resumeParkDescriptionBySession, setResumeParkDescriptionBySession] = useState<Record<string, string>>({});
+  const [resumeAvailableBySession, setResumeAvailableBySession] = useState<Record<string, boolean>>({});
+  const ownerActiveIdRef = useRef(ownerActiveId);
+  ownerActiveIdRef.current = ownerActiveId;
+  const trackerRef = useRef<ReturnType<typeof createResumeAvailabilityTracker> | null>(null);
+  if (trackerRef.current === null) {
+    trackerRef.current = createResumeAvailabilityTracker({
+      query: (sessionId) => services.resume.queryPlan(sessionId),
+      onAvailability: (sessionId, available) => {
+        setResumeAvailableBySession((current) =>
+          current[sessionId] === available ? current : { ...current, [sessionId]: available },
+        );
+      },
+    });
+  }
+  const tracker = trackerRef.current;
+
+  // A session switch re-reads the offer for the session being opened; a stale
+  // answer for the session left behind is keyed by id and cannot bleed across.
+  useEffect(() => {
+    if (ownerActiveId === undefined) return;
+    tracker.request(ownerActiveId);

Review Comment:
   Both halves fixed in d14a5566:
   
   1. The tracker now retracts a visible offer the moment a fresh read starts: `request()` emits `false` when the last emitted value for that session was `true`. A re-read means the cached answer is stale by definition, so the slot is fail-closed until the authority answers again — a session switch (or any other trigger) can no longer leave a clickable stale offer whose click can only park. First reads and already-hidden sessions emit nothing, so the steady state does not flicker.
   2. `rebound` is now a refresh reason alongside `turn-status-change`/`status-change`, and the retraction above covers its in-flight window as well.
   
   Tests: `retracts a visible offer while a fresh read is in flight` pins the synchronous hide and the re-answer, and the burst-coalescing test now asserts the retract-then-re-answer sequence.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(desktop): offer turn resume from the composer send slot [maka]

Posted by "me2seeks (via GitHub)" <gi...@apache.org>.
me2seeks commented on code in PR #5904:
URL: https://github.com/apache/maka/pull/5904#discussion_r4163363502


##########
apps/desktop/src/renderer/application/contracts/resume-availability.ts:
##########
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Authoritative resume availability behind the composer's Resume offer
+ * (#5903). The renderer never derives resumability from local turn state —
+ * the Runtime Host's safe-boundary planner is the only authority, and the
+ * read-only `turn.resume.query` preview it answers is side-effect free.
+ *
+ * The tracker owns the round trips behind one rule: the latest request for a
+ * session wins. A stale answer (an event burst, a session switch mid-flight)
+ * is dropped rather than applied out of order; a parked plan or a failed read
+ * both mean "not offered", keeping the affordance fail-closed. Clicking still
+ * re-runs the full admission inside `turn.resume.start`, so a stale offer can
+ * only ever park, never resume the wrong thing.
+ *
+ * Two fail-closed guards extend that rule:
+ *
+ * - A fresh read retracts the previous offer while it is in flight. The
+ *   cached answer went stale the moment the new question was asked (a
+ *   session switch, a rebound, a status boundary), so the offer hides until
+ *   the authority answers again instead of rendering a clickable stale
+ *   offer whose click can only park (#5904).
+ *
+ * - `noteUserStopped` suppresses the first offer that appears after the
+ *   user stops a Turn. The composer's send slot is where the Stop button
+ *   just sat: offering Resume there a moment later means a repeated or
+ *   late click on Stop restarts the very Turn the user stopped, tool calls
+ *   and spend included. The interrupted-Turn banner remains the deliberate
+ *   resume path for that Turn; the slot offers it only for interruptions
+ *   the user did not just cause. The suppression is exact, not timed: while
+ *   a Turn runs the planner only ever answers `session_busy`, so the first
+ *   `ready` answer after a user stop is that stopped Turn, and it is
+ *   suppressed exactly once.
+ */
+export function createResumeAvailabilityTracker(options: {
+  query(sessionId: string): Promise<{ readonly disposition: 'ready' | 'parked' }>;
+  onAvailability(sessionId: string, available: boolean): void;
+}) {
+  /** Per-session sequence of the newest request; answers older than it are stale. */
+  const latestRequestBySession = new Map<string, number>();
+  /** Per-session last emitted offer, so a re-read retracts only a visible one. */
+  const lastEmittedBySession = new Map<string, boolean>();
+  /** Sessions whose next ready answer is the Turn the user just stopped. */
+  const offerSuppressedAfterUserStop = new Set<string>();
+  const inFlight = new Set<string>();
+  const rerequestWhenSettled = new Set<string>();
+  let sequence = 0;
+
+  function emit(sessionId: string, available: boolean): void {
+    lastEmittedBySession.set(sessionId, available);
+    options.onAvailability(sessionId, available);
+  }
+
+  async function run(sessionId: string, requestId: number): Promise<void> {
+    try {
+      const plan = await options.query(sessionId);
+      if (latestRequestBySession.get(sessionId) !== requestId) return;
+      if (plan.disposition === 'ready' && offerSuppressedAfterUserStop.delete(sessionId)) {

Review Comment:
   Fixed in 70dbcb7e — suppression is now bound to the candidate's identity rather than to an answer count.
   
   The tracker no longer narrows the plan to disposition: `run` reads `sourceTurnId`/`sourceRunId` off the ready plan. `noteUserStopped` arms a per-session capture; the first `ready` answer after the stop (still guaranteed to be the stopped Turn, since a running Turn only ever answers `session_busy`) records the candidate's `(sourceTurnId, sourceRunId)` pair in a per-session suppressed set and emits not-offered. Every later observation of that same pair — including the trailing read produced by the two terminal-notification producers you traced — stays suppressed. The suppression releases on the candidate's lifecycle, not on a read: a resumed-then-re-interrupted Turn comes back under a new source run and offers normally, as does a genuinely different interruption. Plans without identity (test stubs) fall back to suppressing that single answer.
   
   Regression added with two ready answers for the same source Run, per your reproduction: `keeps the stopped candidate hidden across duplicate terminal notifications` (in-flight + trailing read, identical `sourceTurnId`/`sourceRunId`, both hidden). Also `offers again once the resume candidate moves on to a new run or turn` and `keeps the stop capture armed until a ready answer identifies the candidate` (parked answers can neither identify nor spend the capture). Tracker suite 11/11, full desktop dist 3138/3143 (the 5 failures are the known environment-specific ones that reproduce without this PR), typecheck / renderer-architecture `--strict-base` / lint all green.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(desktop): offer turn resume from the composer send slot [maka]

Posted by "me2seeks (via GitHub)" <gi...@apache.org>.
me2seeks commented on PR #5904:
URL: https://github.com/apache/maka/pull/5904#issuecomment-5946495852

   Noted on the residual — yes, that trade is deliberate: a stop whose plan parks keeps the marker armed, so one later slot offer in that session is suppressed and the banner carries it. Fail-closed and self-healing on the next read, which felt right for an affordance whose worst case is a hidden button, not a wrong resume.
   
   Housekeeping: the branch is transplanted onto the rewritten `main` (the upstream history no longer contains this PR's original base, so a normal rebase replayed 400+ commits; the single PR commit was cherry-picked onto `afde01c231` instead, and the renderer-architecture ledger was regenerated from the new tree). Head is now `f60a621557`, `test` is green on it. The PR body screenshots were re-captured with the current play-glyph button, and the Verification numbers reflect the rebased tree.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org