Skip to content

Commit 29a77af

Browse files
Merge posthog-code/finalize-mobile-portability-boundary
Generated-By: PostHog Code Task-Id: c1bbe3cf-742b-4b24-bf96-d11a18b4cf22
2 parents 3e53cc1 + e809724 commit 29a77af

5 files changed

Lines changed: 124 additions & 10 deletions

File tree

apps/mobile/src/features/tasks/hooks/useCloudTaskConfigOptions.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,32 @@ describe("useCloudTaskConfigOptions", () => {
106106
expect(mockGetCloudTaskConfigOptions).toHaveBeenCalledWith("claude");
107107
});
108108

109+
it("replaces a hidden GLM current model with a visible model", async () => {
110+
mockGetCloudTaskConfigOptions.mockResolvedValue([
111+
{
112+
id: "model",
113+
name: "Model",
114+
type: "select",
115+
currentValue: "@cf/zai-org/glm-5.2",
116+
options: [
117+
{ value: "@cf/zai-org/glm-5.2", name: "GLM-5.2" },
118+
{ value: "claude-sonnet-5", name: "Claude Sonnet 5" },
119+
],
120+
category: "model",
121+
description: "Choose a model",
122+
},
123+
] satisfies CloudTaskConfigOption[]);
124+
125+
const result = await renderHook();
126+
await waitForAssertion(() => {
127+
const modelOption = getModelConfigOption(result.current.configOptions);
128+
expect(modelOption.currentValue).toBe("claude-sonnet-5");
129+
expect(modelOption.options.map((option) => option.value)).toEqual([
130+
"claude-sonnet-5",
131+
]);
132+
});
133+
});
134+
109135
it("keeps the shared fallback when unauthenticated", async () => {
110136
mockUseAuthStore.mockImplementation((selector) =>
111137
selector({ oauthAccessToken: null }),

apps/mobile/src/features/tasks/hooks/useCloudTaskConfigOptions.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type CloudTaskConfigOption,
55
GLM_MODEL_FLAG,
66
isGlmModelId,
7+
isRestrictedModelOption,
78
} from "@posthog/shared";
89
import { useQuery } from "@tanstack/react-query";
910
import { useFeatureFlag } from "posthog-react-native";
@@ -35,12 +36,21 @@ export function useCloudTaskConfigOptions(adapter: Adapter = "claude") {
3536
? configOptions
3637
: configOptions.map((option) =>
3738
option.category === "model"
38-
? {
39-
...option,
40-
options: option.options.filter(
39+
? (() => {
40+
const options = option.options.filter(
4141
(model) => !isGlmModelId(model.value),
42-
),
43-
}
42+
);
43+
const currentValue = options.some(
44+
(model) =>
45+
model.value === option.currentValue &&
46+
!isRestrictedModelOption(model._meta),
47+
)
48+
? option.currentValue
49+
: (options.find(
50+
(model) => !isRestrictedModelOption(model._meta),
51+
)?.value ?? option.currentValue);
52+
return { ...option, currentValue, options };
53+
})()
4454
: option,
4555
);
4656

packages/core/src/cloud-task/cloud-task-engine.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class BackendStreamError extends Error {
8181

8282
interface TaskRunResponse {
8383
id: string;
84+
log_url?: string | null;
8485
status: TaskRunStatus;
8586
stage?: string | null;
8687
output?: Record<string, unknown> | null;
@@ -1106,7 +1107,10 @@ export class CloudTaskEngine extends TypedEventEmitter<CloudTaskEvents> {
11061107
}
11071108

11081109
if (isTerminalStatus(run.status)) {
1109-
const historicalEntries = await this.fetchAllSessionLogs(watcher);
1110+
let historicalEntries = await this.fetchAllSessionLogs(watcher);
1111+
if (historicalEntries?.length === 0 && run.log_url) {
1112+
historicalEntries = await this.fetchArchivedLogs(run.log_url);
1113+
}
11101114
const terminalWatcher = this.watchers.get(key);
11111115
if (!terminalWatcher || terminalWatcher !== watcher) return;
11121116
if (watcher.failed) return;
@@ -2154,6 +2158,24 @@ export class CloudTaskEngine extends TypedEventEmitter<CloudTaskEvents> {
21542158
}
21552159
}
21562160

2161+
private async fetchArchivedLogs(
2162+
logUrl: string,
2163+
): Promise<StoredLogEntry[] | null> {
2164+
try {
2165+
const response = await this.streamFetch(logUrl);
2166+
if (!response.ok) return null;
2167+
const content = await response.text();
2168+
if (!content.trim()) return [];
2169+
return content
2170+
.trim()
2171+
.split("\n")
2172+
.map((line) => JSON.parse(line) as StoredLogEntry);
2173+
} catch (error) {
2174+
this.log.warn("Cloud task archived logs fetch error", { logUrl, error });
2175+
return null;
2176+
}
2177+
}
2178+
21572179
private async resolveStreamTarget(watcher: WatcherState): Promise<void> {
21582180
const url = `${watcher.apiHost}/api/projects/${watcher.teamId}/tasks/${watcher.taskId}/runs/${watcher.runId}/stream_token/`;
21592181
try {

packages/core/src/cloud-task/cloud-task.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2532,6 +2532,54 @@ describe("CloudTaskEngine", () => {
25322532
expect(statusFetchCount).toBeLessThanOrEqual(2);
25332533
});
25342534

2535+
it("loads archived logs when a terminal run has no persisted session logs", async () => {
2536+
const updates: unknown[] = [];
2537+
service.on(CloudTaskEvent.Update, (payload) => updates.push(payload));
2538+
const archivedEntry = {
2539+
type: "notification",
2540+
timestamp: "2026-01-01T00:00:00Z",
2541+
};
2542+
2543+
mockNetFetch.mockImplementation((input: string | Request) => {
2544+
const url = typeof input === "string" ? input : input.url;
2545+
if (url.includes("/session_logs/")) {
2546+
return Promise.resolve(
2547+
createJsonResponse([], 200, { "X-Has-More": "false" }),
2548+
);
2549+
}
2550+
if (url === "https://logs.example.com/run-1.jsonl") {
2551+
return Promise.resolve(
2552+
new Response(`${JSON.stringify(archivedEntry)}\n`, { status: 200 }),
2553+
);
2554+
}
2555+
return Promise.resolve(
2556+
createJsonResponse({
2557+
id: "run-1",
2558+
status: "completed",
2559+
log_url: "https://logs.example.com/run-1.jsonl",
2560+
updated_at: "2026-01-01T00:00:00Z",
2561+
}),
2562+
);
2563+
});
2564+
2565+
service.watch({
2566+
taskId: "task-1",
2567+
runId: "run-1",
2568+
apiHost: "https://app.example.com",
2569+
teamId: 2,
2570+
});
2571+
2572+
await waitFor(() => updates.length === 1);
2573+
expect(updates[0]).toEqual(
2574+
expect.objectContaining({
2575+
kind: "snapshot",
2576+
newEntries: [archivedEntry],
2577+
totalEntryCount: 1,
2578+
status: "completed",
2579+
}),
2580+
);
2581+
});
2582+
25352583
const guardedFetchStatusExpectations = [
25362584
[
25372585
401,

packages/core/src/inbox/engagement.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,27 @@ export type InboxViewedFilterState =
199199
| DesktopInboxViewedFilterState
200200
| MobileInboxViewedFilterState;
201201

202-
export interface BuildInboxViewedInput {
202+
interface BuildInboxViewedInputBase {
203203
/**
204204
* Reports currently visible to the user (after reviewer scope + search), used
205205
* for `report_count`, `ready_count`, and the priority/actionability breakdown.
206206
*/
207207
visibleReports: SignalReport[];
208208
/** Server-reported total of reports matching the active query — the headline inbox number. */
209209
totalCount: number;
210-
/** Tab badge counts shown in the v2 header (the numbers the user actually sees). */
211-
tabCounts?: { pulls: number; reports: number };
212-
filters: InboxViewedFilterState;
213210
}
214211

212+
export type BuildInboxViewedInput =
213+
| (BuildInboxViewedInputBase & {
214+
filters: DesktopInboxViewedFilterState;
215+
/** Tab badge counts shown in the desktop header. */
216+
tabCounts: { pulls: number; reports: number };
217+
})
218+
| (BuildInboxViewedInputBase & {
219+
filters: MobileInboxViewedFilterState;
220+
tabCounts?: never;
221+
});
222+
215223
/**
216224
* Build the property payload for the `Inbox viewed` analytics event from the
217225
* v2 inbox state. Pure so it can be unit-tested and reused across hosts.

0 commit comments

Comments
 (0)