Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d438377
feat(native-chat): record which agent run wrote each journal item
brennanb2025 Oct 12, 2026
db574ae
feat(native-chat): one current-work projection for every host reader;…
brennanb2025 Oct 12, 2026
31424b0
feat(native-chat): clients read the host's working answer; refuse ans…
brennanb2025 Oct 12, 2026
7f946e4
refactor(native-chat): keep structuredAgentSessionHandsOver file-loca…
brennanb2025 Oct 13, 2026
d6925c4
test(native-chat): fewer provenance tests: fold reopen and unstated-o…
brennanb2025 Oct 13, 2026
625f5ea
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 13, 2026
362532e
test(native-chat): drop current-work mirrors: the viewKey unit case a…
brennanb2025 Oct 13, 2026
0379f2c
Merge branch 'brennanb2025/settle-split-2' into brennanb2025/settle-s…
brennanb2025 Oct 13, 2026
4c6cbff
test(native-chat): fewer client tests: one flow per client, one fallb…
brennanb2025 Oct 13, 2026
f0a45c2
test(native-chat): an unstamped roster entry from an older build read…
brennanb2025 Oct 13, 2026
3f47bac
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 13, 2026
b4177c9
Merge branch 'brennanb2025/settle-split-2' into brennanb2025/settle-s…
brennanb2025 Oct 13, 2026
2e19393
Merge remote-tracking branch 'origin/main' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
ada76fe
test(native-chat): build the older-build roster row inline, after mai…
brennanb2025 Oct 14, 2026
75f2c36
Merge remote-tracking branch 'origin/main' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
d4471e8
refactor(native-chat): provenance is per item only; drop per-entry st…
brennanb2025 Oct 14, 2026
7cace4b
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
a0e0eb6
Merge branch 'brennanb2025/settle-split-2' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
efa2676
fix(native-chat): an item's provenance is set when it is created; rev…
brennanb2025 Oct 14, 2026
28edc8d
test(native-chat): keep the provenance test below the production line…
brennanb2025 Oct 14, 2026
00e7f56
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
387d313
Merge remote-tracking branch 'origin/main' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
a9b74f4
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
c21f7cc
Merge branch 'brennanb2025/settle-split-2' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
e495841
fix(native-chat): an ended agent's card Cancel commits with its recei…
brennanb2025 Oct 14, 2026
3dce312
Merge origin/main (3bfb1149da6, Part 2's squash) into settle-split pa…
brennanb2025 Oct 15, 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
1 change: 1 addition & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-late-proof-tool-ends.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-ended-prompt-cancel.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-legacy-handoff-record.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-live-turn-window.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-main-agent-working-agreement.test.ts',
Expand Down
33 changes: 33 additions & 0 deletions mobile/src/session/mobile-structured-actionable-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { isActionableStructuredAgentSessionPrompt } from '../../../src/shared/structured-agent-session-live-turn'
import type { StructuredAgentSessionState } from '../../../src/shared/structured-agent-session-reducer'
import {
pendingStructuredApproval,
pendingStructuredQuestion,
type StructuredApprovalItem,
type StructuredQuestionItem
} from './mobile-structured-agent-prompts'

type PromptState = Pick<StructuredAgentSessionState, 'items' | 'actionablePromptIds'>

/** The approval a person can answer: pending, and named by the host as waiting when it names them
* (`isActionableStructuredAgentSessionPrompt`): one an agent that ended raised is not. */
export function actionableStructuredApproval(state: PromptState): StructuredApprovalItem | null {
return (
state.items.find(
(item): item is StructuredApprovalItem =>
pendingStructuredApproval(item) &&
isActionableStructuredAgentSessionPrompt(item.itemId, state.actionablePromptIds)
) ?? null
)
}

/** The question a person can answer, by the same rule. */
export function actionableStructuredQuestion(state: PromptState): StructuredQuestionItem | null {
return (
state.items.find(
(item): item is StructuredQuestionItem =>
pendingStructuredQuestion(item) &&
isActionableStructuredAgentSessionPrompt(item.itemId, state.actionablePromptIds)
) ?? null
)
}
14 changes: 9 additions & 5 deletions mobile/src/session/mobile-structured-agent-session-cancel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { AgentSessionCancelResult } from '../../../src/shared/agent-session-wire'
import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types'
import type { StructuredAgentSessionState } from '../../../src/shared/structured-agent-session-reducer'
import { runningStructuredAgentSessionTurnId } from '../../../src/shared/structured-agent-session-live-turn'
import {
isActionableStructuredAgentSessionPrompt,
runningStructuredAgentSessionTurnId
} from '../../../src/shared/structured-agent-session-live-turn'
import type { RpcClient } from '../transport/rpc-client'
import {
requestStructuredAgentSessionMutation,
Expand All @@ -10,12 +12,14 @@ import {

type PromptIdentity = { itemId: string; expectedRevision: number }

/** The prompt a Stop cancels: one the host says still waits on the person, when it says. */
export function pendingStructuredPromptIdentity(
items: readonly AgentJournalRenderItem[]
state: Pick<StructuredAgentSessionState, 'items' | 'actionablePromptIds'>
): PromptIdentity | undefined {
const prompt = items.find((item) =>
const prompt = state.items.find((item) =>
item.body.kind === 'approval' || item.body.kind === 'question'
? item.body.resolution.state === 'pending'
? item.body.resolution.state === 'pending' &&
isActionableStructuredAgentSessionPrompt(item.itemId, state.actionablePromptIds)
: false
)
return prompt ? { itemId: prompt.itemId, expectedRevision: prompt.revision } : undefined
Expand Down
11 changes: 8 additions & 3 deletions mobile/src/session/mobile-structured-agent-session-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export async function requestStructuredAgentSessionMutation<TValue>(args: {
fields: Record<string, unknown>
clientOperationId?: string
timeoutMs?: number
/** The chat's agent, which a refusal about it names. */
agentName?: string
}): Promise<StructuredAgentSessionMutationCallResult<TValue>> {
const {
client,
Expand All @@ -136,8 +138,10 @@ export async function requestStructuredAgentSessionMutation<TValue>(args: {
expectedRuntimeFence,
fields,
clientOperationId,
timeoutMs
timeoutMs,
agentName
} = args
const context = agentName ? { agentName } : {}
try {
const result = await callAgentSession<AgentSessionMutationResult<TValue>>(
client,
Expand Down Expand Up @@ -171,7 +175,8 @@ export async function requestStructuredAgentSessionMutation<TValue>(args: {
code: result.refusal.code,
message: agentSessionRefusalNotice(
result.refusal,
phoneWriteKind(fingerprintMethod, fields)
phoneWriteKind(fingerprintMethod, fields),
context
)
}
} catch (error) {
Expand All @@ -184,7 +189,7 @@ export async function requestStructuredAgentSessionMutation<TValue>(args: {
return {
status: 'failed',
message: agentSessionWriteNoticeEnglish(
agentSessionWriteNoticeParts(answered, phoneWriteKind(fingerprintMethod, fields))
agentSessionWriteNoticeParts(answered, phoneWriteKind(fingerprintMethod, fields), context)
)
}
}
Expand Down
3 changes: 2 additions & 1 deletion mobile/src/session/use-mobile-native-chat-turn-disclosure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ export function useMobileNativeChatTurnDisclosure({
rows,
turnJournal?.items,
stopping,
turnJournal?.submissions
turnJournal?.submissions,
turnJournal ?? {}
)
: null
if (!ids?.size) {
Expand Down
9 changes: 6 additions & 3 deletions mobile/src/session/use-mobile-structured-agent-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ export function useMobileStructuredAgentMutate(args: {
enabled: boolean
stateRef: { readonly current: StructuredAgentSessionState }
onSendError: (message: string) => void
/** The chat's agent, which a refusal about it names. */
agentName?: string
}): MobileStructuredAgentMutate {
const { client, enabled, onSendError, sessionId, stateRef } = args
const { agentName, client, enabled, onSendError, sessionId, stateRef } = args
return useCallback(
async <TValue>(
method: string,
Expand All @@ -41,7 +43,8 @@ export function useMobileStructuredAgentMutate(args: {
fingerprintMethod,
sessionId,
expectedRuntimeFence: targetFence,
fields
fields,
...(agentName ? { agentName } : {})
})
if (result.status === 'accepted') {
return {
Expand All @@ -56,6 +59,6 @@ export function useMobileStructuredAgentMutate(args: {
onSendError(result.message)
return { status: 'rejected' }
},
[client, enabled, onSendError, sessionId, stateRef]
[agentName, client, enabled, onSendError, sessionId, stateRef]
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,22 @@ describe('mobile structured prompt cancellation', () => {
expect(hook.turnId).toBeNull()
})

it("reads Working from the host's answer, and from its rows only when an older host gives none", () => {
// A turn row an agent that has since ended left running: the host says it is not working.
state = { ...state, items: [runningTurn()], working: false }
act(() => {
renderer = create(createElement(Harness, { promptCancelSupported: true }))
})
expect(hook.isWorking).toBe(false)
act(() => renderer?.unmount())
const { working: _answered, ...olderHost } = state
state = olderHost
act(() => {
renderer = create(createElement(Harness, { promptCancelSupported: true }))
})
expect(hook.isWorking).toBe(true)
})

it('uses the rendered prompt identity when the journal changes before tap', async () => {
act(() => {
renderer = create(createElement(Harness, { promptCancelSupported: true }))
Expand Down
34 changes: 19 additions & 15 deletions mobile/src/session/use-mobile-structured-agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import { isStructuredAgentSessionMainAgentWorking } from '../../../src/shared/st
import { isFinalAgentSessionReadRefusal } from '../../../src/shared/structured-agent-session-read-refusal'
import {
isStructuredAgentSessionThinking,
runningStructuredAgentSessionTurnId
runningStructuredAgentSessionTurnId,
structuredAgentSessionHostSaysWorking
} from '../../../src/shared/structured-agent-session-live-turn'
import { selectStructuredAgentTurnActivity } from '../../../src/shared/native-chat-turn-activity'
import {
pendingStructuredApproval,
pendingStructuredQuestion,
actionableStructuredApproval,
actionableStructuredQuestion
} from './mobile-structured-actionable-prompt'
import {
projectStructuredPermission,
projectStructuredQuestion
} from './mobile-structured-agent-prompts'
Expand Down Expand Up @@ -91,7 +94,8 @@ export function useMobileStructuredAgentSession(args: {
sessionId,
enabled,
stateRef,
onSendError
onSendError,
...(agent ? { agentName: tuiAgentDisplayName(agent) ?? agent } : {})
})

const optionStamp = useMobileStructuredSessionOptionStamp({
Expand Down Expand Up @@ -159,7 +163,10 @@ export function useMobileStructuredAgentSession(args: {
const activityText =
selectStructuredAgentTurnActivity(state.items, turnId, state.activity)?.text ?? null
const thinking = isStructuredAgentSessionThinking(state)
const isWorking = isStructuredAgentSessionMainAgentWorking(turnId, state.submissions, state.fence)
// The host's answer counts only the agent running now; an older host gives none.
const isWorking = structuredAgentSessionHostSaysWorking(state.working, () =>
isStructuredAgentSessionMainAgentWorking(turnId, state.submissions, state.fence)
)
const backgroundTasks = useMobileStructuredBackgroundTasks({
sessionKey,
state,
Expand Down Expand Up @@ -214,14 +221,11 @@ export function useMobileStructuredAgentSession(args: {
[thinking, activityText, stopping, stopRequestInFlight, queueCapable, state.items]
)
const status = state.status === 'idle' ? 'idle' : state.status
const approvalPrompt = useMemo(
() => state.items.find(pendingStructuredApproval) ?? null,
[state.items]
)
const questionPrompt = useMemo(
() => state.items.find(pendingStructuredQuestion) ?? null,
[state.items]
)
// The host's answer where it gives one: a prompt an agent that ended raised takes no answer.
const [approvalPrompt, questionPrompt] = useMemo(() => {
const prompts = { items: state.items, actionablePromptIds: state.actionablePromptIds }
return [actionableStructuredApproval(prompts), actionableStructuredQuestion(prompts)] as const
}, [state.items, state.actionablePromptIds])
// What a refused command's line on the phone stands on.
const commandRefusalCauses = useMemo(
() => ({
Expand All @@ -237,7 +241,7 @@ export function useMobileStructuredAgentSession(args: {
queuedMessages,
queuePause,
submissions: state.submissions,
agentWorking: isStructuredAgentSessionMainAgentWorking(turnId, state.submissions, state.fence),
agentWorking: isWorking,
mutate,
appendComposerText,
onSendError,
Expand Down Expand Up @@ -299,7 +303,7 @@ export function useMobileStructuredAgentSession(args: {
void stopPress.track(() => requestCancel())
},
cancelPrompt: (prompt?: { itemId: string; expectedRevision: number }) =>
requestCancel(prompt ?? pendingStructuredPromptIdentity(stateRef.current.items)),
requestCancel(prompt ?? pendingStructuredPromptIdentity(stateRef.current)),
permission: projectStructuredPermission(approvalPrompt),
question: projectStructuredQuestion(questionPrompt, groupedDraft),
respondPermission,
Expand Down
7 changes: 5 additions & 2 deletions mobile/src/session/use-mobile-structured-agent-turn-timing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ export function useMobileStructuredAgentTurnTiming(
items,
submissions,
latestTurn,
working,
hostClock
}: {
items: readonly AgentJournalRenderItem[]
submissions: readonly AgentJournalSubmission[]
latestTurn?: AgentSessionLatestTurn | null
/** The host's working answer, which places rows waiting behind the live turn. */
working?: boolean
hostClock?: StructuredAgentHostClock | null
},
turnId: string | null
Expand All @@ -40,8 +43,8 @@ export function useMobileStructuredAgentTurnTiming(
[items, submissions, turnId, latestTurn]
)
const turnJournal = useMemo(
() => ({ items, submissions, latestTurn }),
[items, submissions, latestTurn]
() => ({ items, submissions, latestTurn, ...(working !== undefined ? { working } : {}) }),
[items, submissions, latestTurn, working]
)
const [latch, setLatch] = useState<StructuredAgentTurnClockLatch | null>(null)
// Stamp during render (React's derive-from-props pattern) so the first paint of
Expand Down
10 changes: 6 additions & 4 deletions mobile/src/session/use-mobile-structured-prompt-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
} from '../../../src/shared/agent-session-question-answer'
import type { StructuredAgentSessionState } from '../../../src/shared/structured-agent-session-reducer'
import {
pendingStructuredApproval,
pendingStructuredQuestion,
actionableStructuredApproval,
actionableStructuredQuestion
} from './mobile-structured-actionable-prompt'
import {
structuredApprovalResponseTarget,
structuredQuestionResponseTarget
} from './mobile-structured-agent-prompts'
Expand Down Expand Up @@ -54,7 +56,7 @@ export function useMobileStructuredPromptResponses(args: {
async (optionId: string): Promise<boolean> => {
const target = structuredApprovalResponseTarget(
optionId,
stateRef.current.items.find(pendingStructuredApproval) ?? null
actionableStructuredApproval(stateRef.current)
)
if (!target) {
return false
Expand Down Expand Up @@ -107,7 +109,7 @@ export function useMobileStructuredPromptResponses(args: {

const respondQuestion = useCallback(
async (answer: string): Promise<boolean> => {
const prompt = stateRef.current.items.find(pendingStructuredQuestion) ?? null
const prompt = actionableStructuredQuestion(stateRef.current)
if (prompt?.body.questions) {
const promptKey = groupedQuestionPromptKey(prompt.itemId, prompt.revision)
const grouped = advanceGroupedQuestion({
Expand Down
11 changes: 5 additions & 6 deletions mobile/src/session/use-mobile-structured-send-with-outcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { structuredSessionOperationId } from './structured-session-operation-id'
import { timeoutForDeadline } from './mobile-structured-agent-session-rpc'
import { structuredAgentSessionSendSelection } from '../../../src/shared/structured-agent-session-option-view'
import {
pendingStructuredApproval,
pendingStructuredQuestion
} from './mobile-structured-agent-prompts'
actionableStructuredApproval,
actionableStructuredQuestion
} from './mobile-structured-actionable-prompt'

/** Whether a send made now asks the host to queue it. The host's queue waits on any pending prompt;
* one this build cannot answer would hold the send forever, so it starts a turn instead. */
Expand Down Expand Up @@ -113,9 +113,8 @@ export function useMobileStructuredSendWithOutcome(args: {
pending: commandPending,
controller: { agent: agent ?? undefined, ...controller },
busy: () =>
stateRef.current.items.some(
(item) => pendingStructuredApproval(item) || pendingStructuredQuestion(item)
)
actionableStructuredApproval(stateRef.current) ||
actionableStructuredQuestion(stateRef.current)
? 'prompt'
: runningStructuredAgentSessionTurnId(stateRef.current)
? 'working'
Expand Down
Loading
Loading