Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
dd552ad
feat(analytics): add shared event contract
iscekic Aug 5, 2026
f11f6b0
feat(db): add operation ledger and analytics outbox
iscekic Aug 5, 2026
82f89a4
feat(analytics): add outbox dispatch cron
iscekic Aug 6, 2026
5368a60
feat(mobile): track settled leaf screens
iscekic Aug 6, 2026
6695dbc
feat(cloud-agent): make session creation retry safe
iscekic Aug 6, 2026
2b91bdf
fix(db): record progress during session reconciliation
iscekic Aug 6, 2026
08155b7
fix(db): serialize ledger reconciliation by lease
iscekic Aug 6, 2026
55a4160
feat(pr-review): make mutations retry safe
iscekic Aug 6, 2026
f3f913d
feat(security): make security and org writes retry safe
iscekic Aug 6, 2026
5c22793
feat(cloud-agent-sdk): dedupe remote session creation
iscekic Aug 6, 2026
bc04d2e
test(session-ingest): update attention signal assertion
iscekic Aug 6, 2026
368e07e
fix(cloud-agent-sdk): verify remote replay classification
iscekic Aug 6, 2026
3360aaf
fix(security): guard optional command ids
iscekic Aug 6, 2026
ec8f5a2
fix(analytics): await flush and key security dismissals
iscekic Aug 6, 2026
dd4dc17
feat(mobile): stabilize session creation operation keys
iscekic Aug 6, 2026
095738b
fix(mobile): verify remote operation key tests
iscekic Aug 6, 2026
1035604
fix(security): repair disabled routing and dismissal states
iscekic Aug 6, 2026
eee1144
fix(mobile): preserve remote retry keys
iscekic Aug 6, 2026
43d178f
fix(cloud): preserve fallback and settlement recovery
iscekic Aug 6, 2026
32e8065
fix(security): retain terminal settlement and block retry
iscekic Aug 6, 2026
489c00c
fix(ledger): make security acceptance atomic
iscekic Aug 6, 2026
76be0f4
test(db): cover atomic security acceptance
iscekic Aug 6, 2026
ffb8558
merge: sync main into audit branch
iscekic Aug 6, 2026
aa9c0e4
fix(db): reconcile merged migration history
iscekic Aug 6, 2026
0646c4f
fix(mobile): restore organization key narrowing
iscekic Aug 6, 2026
1e9e489
fix(ledger): preserve reconciliation evidence
iscekic Aug 6, 2026
d7a50be
fix(cloud): recheck metadata before ownership delete
iscekic Aug 6, 2026
f5fb286
merge: sync latest main changes
iscekic Aug 6, 2026
f0c9709
fix(organizations): adopt latest role APIs
iscekic Aug 6, 2026
461c963
fix(db): register operation ledger migration
iscekic Aug 6, 2026
ffa7e88
fix(mobile): remove unused analytics exports
iscekic Aug 6, 2026
619664f
fix(cloud): harden session reconciliation
iscekic Aug 6, 2026
98ae945
fix(analytics): harden privacy key matching
iscekic Aug 6, 2026
f662fd8
fix(cloud): bind session retries to intent
iscekic Aug 6, 2026
be276b9
fix(org-members): settle authorization failures
iscekic Aug 6, 2026
e8a5806
fix(security): classify missing service config
iscekic Aug 6, 2026
5041c4f
fix(mobile): rotate share spawn keys after ready
iscekic Aug 6, 2026
4f457fc
refactor(mobile): remove dormant prepare key path
iscekic Aug 6, 2026
0ef9e49
fix(org-members): retry removal cleanup
iscekic Aug 6, 2026
69a4398
fix(security): fence keyed queue retries
iscekic Aug 7, 2026
0caa181
fix(mobile): isolate operation retry state
iscekic Aug 7, 2026
b441a07
fix(mobile): contain continuation post-success failures
iscekic Aug 7, 2026
3c89f12
Merge remote-tracking branch 'origin/main' into audit-w3b-operation-l…
iscekic Aug 7, 2026
836d1ca
chore: retrigger review
iscekic Aug 7, 2026
6b1397c
chore: retrigger review
iscekic Aug 7, 2026
7d8bfa8
merge: sync latest main
iscekic Aug 17, 2026
60faa4d
fix(mobile): update native test mocks
iscekic Aug 17, 2026
a27fdeb
refactor(cloud): simplify operation ledgers and event contract
iscekic Aug 17, 2026
ddd8ac6
fix(cloud): remove redundant ledger reads and correct audit copy
iscekic Aug 17, 2026
8fe4359
refactor(cloud): share runAfterResponse and sha256 helpers
iscekic Aug 17, 2026
9aa0a0b
refactor(organizations): drop the operation ledger from member writes
iscekic Aug 17, 2026
d7c400d
refactor(pr-review): share the intent fingerprint across web and mobile
iscekic Aug 17, 2026
38cba03
refactor(security): key command retries on the command row
iscekic Aug 17, 2026
c8f6912
refactor(mobile): move the operation key out of the PR merge module
iscekic Aug 17, 2026
cc12708
refactor(mobile): split the two hooks that silenced max-lines
iscekic Aug 17, 2026
fbf6b97
merge: sync latest main
iscekic Aug 17, 2026
51c6fb7
fix(cloud-agent-next): abandon a create whose RPC is lost
iscekic Aug 17, 2026
28dec1d
refactor(db): squash the ledger migrations and partial-index provider…
iscekic Aug 17, 2026
5b121f8
fix(security): settle command ledgers in worker
iscekic Aug 17, 2026
4cdebb1
fix(security): preserve dismissal analytics identity
iscekic Aug 17, 2026
f628e57
fix: address operation ledger review findings
iscekic Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,10 @@ function RootLayoutNav() {
accountId: userId,
optionalConsent,
});
useScreenTracking();
// Screen capture must wait for consent: analytics eligibility is decided
// only after the account's consent decision has loaded without error.
const bootstrapSettled = token != null && consentChecked && !needsConsent && !consentCheckError;
useScreenTracking(bootstrapSettled);

useEffect(() => {
if (shareIntentError) {
Expand Down
52 changes: 51 additions & 1 deletion apps/mobile/src/components/agents/mobile-session-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable require-await, @typescript-eslint/require-await -- injectable query/sleep fakes settle without await */
/* eslint-disable max-lines -- the manager suite pins retry cadence and attachment mints in one file. */
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { type AgentAttachmentSubmissionPayload } from '@/lib/agent-attachments/agent-attachment-types';
Expand Down Expand Up @@ -41,17 +42,22 @@ vi.mock('@/components/agents/tool-card-image-cache', () => ({
}));

const mutate = vi.fn();
const prepareSessionMutate = vi.fn();
vi.mock('@/lib/trpc', () => ({
trpcClient: {
cloudAgentNext: {
getAttachmentDownloadUrl: { mutate },
prepareSession: { mutate: prepareSessionMutate },
},
organizations: {
cloudAgentNext: { prepareSession: { mutate: prepareSessionMutate } },
},
},
}));

const { buildRemoteAttachmentParts } =
await import('@/components/agents/mobile-session-manager-helpers');
const { fetchSessionWithNotFoundRetry, readFetchSessionErrorCode } =
const { fetchSessionWithNotFoundRetry, isCloudPrepareRetryableError, readFetchSessionErrorCode } =
await import('@/components/agents/mobile-session-manager');

const SESSION_ID = 'ses_test_session_id_0000000001' as KiloSessionId;
Expand All @@ -62,6 +68,10 @@ function notFoundError(): Error {
return error;
}

function withCode(code: string, message: string): Error {
return Object.assign(new Error(message), { data: { code } });
}

describe('buildRemoteAttachmentParts', () => {
beforeEach(() => {
mutate.mockReset();
Expand Down Expand Up @@ -189,6 +199,46 @@ describe('readFetchSessionErrorCode', () => {
});
});

describe('isCloudPrepareRetryableError', () => {
it('keeps the key for creation_in_progress (CONFLICT)', () => {
expect(isCloudPrepareRetryableError(withCode('CONFLICT', 'creation_in_progress'))).toBe(true);
});

it('keeps the key for a network error with no tRPC code', () => {
expect(isCloudPrepareRetryableError(new Error('Network request failed'))).toBe(true);
});

it('keeps the key for transient 5xx-class and rate-limit codes', () => {
for (const code of [
'INTERNAL_SERVER_ERROR',
'BAD_GATEWAY',
'SERVICE_UNAVAILABLE',
'GATEWAY_TIMEOUT',
'TIMEOUT',
'TOO_MANY_REQUESTS',
]) {
expect(isCloudPrepareRetryableError(withCode(code, 'boom'))).toBe(true);
}
});

it('rotates the key on typed terminal rejections', () => {
for (const code of [
'BAD_REQUEST',
'UNAUTHORIZED',
'FORBIDDEN',
'NOT_FOUND',
'PAYMENT_REQUIRED',
'PRECONDITION_FAILED',
]) {
expect(isCloudPrepareRetryableError(withCode(code, 'nope'))).toBe(false);
}
});

it('rotates the key on a CONFLICT with any other message', () => {
expect(isCloudPrepareRetryableError(withCode('CONFLICT', 'something else'))).toBe(false);
});
});

describe('fetchSessionWithNotFoundRetry', () => {
// Production return type is SessionWithRuntimeState; tests inject a minimal
// stand-in via `query` and only assert retry/cadence behavior.
Expand Down
32 changes: 32 additions & 0 deletions apps/mobile/src/components/agents/mobile-session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,38 @@ export function readFetchSessionErrorCode(error: unknown): string | undefined {
return undefined;
}

/**
* tRPC codes transient enough to keep the same cloud-prepare `operationKey`
* across a retry. Any other typed code is a terminal rejection and rotates it.
*/
const CLOUD_PREPARE_TRANSIENT_CODES = new Set([
'INTERNAL_SERVER_ERROR',
'BAD_GATEWAY',
'SERVICE_UNAVAILABLE',
'GATEWAY_TIMEOUT',
'TIMEOUT',
'TOO_MANY_REQUESTS',
]);

/** Stable message the ledger returns on a same-key in-flight duplicate (plan P1-A-08b). */
const CLOUD_PREPARE_IN_PROGRESS_MESSAGE = 'creation_in_progress';

/**
* True when a `prepareSession` failure may be retried with the SAME
* `operationKey`: `creation_in_progress`, a transient 5xx, or a codeless
* transport failure (the ledger reconciles the ambiguous prior attempt).
*/
export function isCloudPrepareRetryableError(error: unknown): boolean {
const code = readFetchSessionErrorCode(error);
if (code === undefined) {
return true;
}
if (code === 'CONFLICT') {
return error instanceof Error && error.message === CLOUD_PREPARE_IN_PROGRESS_MESSAGE;
}
return CLOUD_PREPARE_TRANSIENT_CODES.has(code);
}

/* eslint-disable @typescript-eslint/promise-function-async, require-await -- thin tRPC passthrough */
async function defaultFetchSessionQuery(
sessionId: KiloSessionId
Expand Down
103 changes: 103 additions & 0 deletions apps/mobile/src/components/agents/use-continue-cloud-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// The cloud-agent leg of `useContinueSession`: one `prepareSession` call, its
// hoisted operation key, and the contained post-success UI work. Split out of
// `use-continue-session.ts` (which keeps the paging drain, destination
// resolution, and the remote spawn leg) so each file stays under the
// max-lines limit.
import { useCallback } from 'react';
import { useRouter } from 'expo-router';
import { useQueryClient } from '@tanstack/react-query';
import { generateMessageId } from '@kilocode/cloud-agent-sdk/message-id';
import * as Haptics from 'expo-haptics';

import { normalizeAgentMode } from '@/components/agents/mode-options';
import { isCloudPrepareRetryableError } from '@/components/agents/mobile-session-manager';
import { getAgentSessionPath } from '@/components/agents/session-detail-routes';
import { useHoistedOperationKey } from '@/lib/operation-key';
import { captureEvent, SESSION_CREATED_EVENT } from '@/lib/analytics/posthog';
import { invalidateAgentSessionQueries } from '@/lib/agent-session-cache';
import { trpcClient, useTRPC } from '@/lib/trpc';

export function useContinueCloudCreate(
organizationId: string | undefined
): (
seed: string,
dest: { repo: string; model: string; variant: string },
mode: string
) => Promise<void> {
const router = useRouter();
const queryClient = useQueryClient();
const trpc = useTRPC();
// P1-A-08b: cloud prepares and remote spawns are different intents, so each
// destination family holds its own hoisted `operationKey`.
const cloudOperationKey = useHoistedOperationKey();

return useCallback(
async (seed: string, dest: { repo: string; model: string; variant: string }, mode: string) => {
const intentFingerprint = JSON.stringify({
seed,
repo: dest.repo,
model: dest.model,
variant: dest.variant || undefined,
mode,
organizationId: organizationId ?? null,
});
const operationKey = cloudOperationKey.getKey(intentFingerprint);
const initialMessageId = generateMessageId();
const baseInput = {
prompt: seed,
initialMessageId,
mode: normalizeAgentMode(mode),
model: dest.model,
variant: dest.variant || undefined,
githubRepo: dest.repo,
autoCommit: true,
autoInitiate: true,
operationKey,
};
try {
const result = organizationId
? await trpcClient.organizations.cloudAgentNext.prepareSession.mutate({
...baseInput,
organizationId,
})
: await trpcClient.cloudAgentNext.prepareSession.mutate(baseInput);
// The intent settled; the next submit is a fresh intent. Rotate
// before the post-success work so a UI failure cannot keep the
// successful key for a retry or rotate it a second time.
cloudOperationKey.rotateKey();

// The cloud session already exists, so no post-success UI failure may
// report the create as failed or invite a duplicate retry. Each step is
// contained on its own so one failure cannot skip the navigation.
try {
captureEvent(SESSION_CREATED_EVENT, { surface: 'cloud-agent' });
} catch {
// Analytics is best-effort; stay silent.
}
try {
await invalidateAgentSessionQueries(queryClient, trpc);
} catch {
// A failed cache invalidation is cosmetic; navigation must still run.
}
try {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch {
// A failed haptics call is cosmetic; stay silent and navigate.
}
try {
router.push(getAgentSessionPath(result.kiloSessionId, organizationId));
} catch {
// A navigation failure is not a create failure.
}
} catch (error) {
// Only `prepareSession` errors reach here; UI failures are contained
// above. A typed terminal rejection ends the intent.
if (!isCloudPrepareRetryableError(error)) {
cloudOperationKey.rotateKey();
}
throw error;
}
},
[organizationId, queryClient, router, trpc, cloudOperationKey]
);
}
Loading
Loading