| author | |
| committer | |
| log | b2e2ccfdb462c45a8de394ff9ff0f58a4a689900 |
| tree | c47d962e95381a1c22a4d2f8dadcefb1dbf72ea2 |
| parent | 792a1404f6d25838597f8f31b0af144dc1beee12 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 96 insertions(+), 18 deletions(-)
apps/server/src/orchestration/ActivityPayloadProjection.test.ts+40-1| ... | @@ -20,7 +20,7 @@ function activity(payload: Record<string, unknown>): OrchestrationThreadActivity | ... | @@ -20,7 +20,7 @@ function activity(payload: Record<string, unknown>): OrchestrationThreadActivity |
| 20 | * If slimming ever moves to an allowlist over the whole payload, these | 20 | * If slimming ever moves to an allowlist over the whole payload, these |
| 21 | * assertions are the tripwire. | 21 | * assertions are the tripwire. |
| 22 | */ | 22 | */ |
| 23 | describe("projectActivityPayload agent-field survival", () => { | 23 | describe("projectActivityPayload", () => { |
| 24 | it("preserves tool attribution (agentId/parentToolUseId) through data slimming", () => { | 24 | it("preserves tool attribution (agentId/parentToolUseId) through data slimming", () => { |
| 25 | const projected = projectActivityPayload( | 25 | const projected = projectActivityPayload( |
| 26 | activity({ | 26 | activity({ |
| ... | @@ -97,6 +97,45 @@ describe("projectActivityPayload agent-field survival", () => { | ... | @@ -97,6 +97,45 @@ describe("projectActivityPayload agent-field survival", () => { |
| 97 | expect(JSON.stringify(acp.payload).length).toBeLessThan(500); | 97 | expect(JSON.stringify(acp.payload).length).toBeLessThan(500); |
| 98 | }); | 98 | }); |
| 99 | 99 | ||
| 100 | it("normalizes Claude and OpenCode command inputs before slimming provider data", () => { | ||
| 101 | const claude = projectActivityPayload( | ||
| 102 | activity({ | ||
| 103 | itemType: "command_execution", | ||
| 104 | toolCallId: "claude-call-1", | ||
| 105 | data: { | ||
| 106 | toolName: "Bash", | ||
| 107 | input: { command: "vp test run" }, | ||
| 108 | result: { content: "x".repeat(5_000) }, | ||
| 109 | }, | ||
| 110 | }), | ||
| 111 | ); | ||
| 112 | const openCode = projectActivityPayload( | ||
| 113 | activity({ | ||
| 114 | itemType: "command_execution", | ||
| 115 | toolCallId: "opencode-call-1", | ||
| 116 | data: { | ||
| 117 | tool: "bash", | ||
| 118 | state: { | ||
| 119 | status: "running", | ||
| 120 | input: { command: "vp lint" }, | ||
| 121 | output: "x".repeat(5_000), | ||
| 122 | }, | ||
| 123 | }, | ||
| 124 | }), | ||
| 125 | ); | ||
| 126 | |||
| 127 | expect(claude.payload).toMatchObject({ | ||
| 128 | toolCallId: "claude-call-1", | ||
| 129 | data: { command: "vp test run" }, | ||
| 130 | }); | ||
| 131 | expect(openCode.payload).toMatchObject({ | ||
| 132 | toolCallId: "opencode-call-1", | ||
| 133 | data: { command: "vp lint" }, | ||
| 134 | }); | ||
| 135 | expect(JSON.stringify(claude.payload).length).toBeLessThan(200); | ||
| 136 | expect(JSON.stringify(openCode.payload).length).toBeLessThan(200); | ||
| 137 | }); | ||
| 138 | |||
| 100 | it("slims Codex-shaped mcp_tool_call items to rendered fields plus a result summary", () => { | 139 | it("slims Codex-shaped mcp_tool_call items to rendered fields plus a result summary", () => { |
| 101 | const projected = projectActivityPayload( | 140 | const projected = projectActivityPayload( |
| 102 | activity({ | 141 | activity({ |
apps/server/src/orchestration/ActivityPayloadProjection.ts+27-9| ... | @@ -125,6 +125,24 @@ function projectCommandData(data: Record<string, unknown>): Record<string, unkno | ... | @@ -125,6 +125,24 @@ function projectCommandData(data: Record<string, unknown>): Record<string, unkno |
| 125 | return Object.keys(projectedItem).length > 0 ? projectedItem : undefined; | 125 | return Object.keys(projectedItem).length > 0 ? projectedItem : undefined; |
| 126 | } | 126 | } |
| 127 | 127 | ||
| 128 | function projectCommandValue(data: Record<string, unknown>): unknown { | ||
| 129 | if (data.command !== undefined) { | ||
| 130 | return data.command; | ||
| 131 | } | ||
| 132 | |||
| 133 | const input = asRecord(data.input); | ||
| 134 | if (input?.command !== undefined) { | ||
| 135 | return input.command; | ||
| 136 | } | ||
| 137 | |||
| 138 | const stateInput = asRecord(asRecord(data.state)?.input); | ||
| 139 | if (stateInput?.command !== undefined) { | ||
| 140 | return stateInput.command; | ||
| 141 | } | ||
| 142 | |||
| 143 | return undefined; | ||
| 144 | } | ||
| 145 | |||
| 128 | function summarizeToolTextOutput(value: string): string | null { | 146 | function summarizeToolTextOutput(value: string): string | null { |
| 129 | const lines: string[] = []; | 147 | const lines: string[] = []; |
| 130 | for (const rawLine of value.split(/\r?\n/u)) { | 148 | for (const rawLine of value.split(/\r?\n/u)) { |
| ... | @@ -339,8 +357,9 @@ export function projectActivityPayload( | ... | @@ -339,8 +357,9 @@ export function projectActivityPayload( |
| 339 | if (item) { | 357 | if (item) { |
| 340 | projectedData.item = item; | 358 | projectedData.item = item; |
| 341 | } | 359 | } |
| 342 | if ("command" in data) { | 360 | const command = projectCommandValue(data); |
| 343 | projectedData.command = data.command; | 361 | if (command !== undefined) { |
| 362 | projectedData.command = command; | ||
| 344 | } | 363 | } |
| 345 | 364 | ||
| 346 | const changedFiles: string[] = []; | 365 | const changedFiles: string[] = []; |
| ... | @@ -418,12 +437,10 @@ function dropStaleContextWindowActivities( | ... | @@ -418,12 +437,10 @@ function dropStaleContextWindowActivities( |
| 418 | } | 437 | } |
| 419 | 438 | ||
| 420 | /** | 439 | /** |
| 421 | * Identity both clients use to fold a tool lifecycle row into the call it | 440 | * Identity used to retain only the newest lifecycle row for each call in a |
| 422 | * belongs to (`deriveToolLifecycleCollapseKey` in web's `session-logic` and | 441 | * thread snapshot. Prefer the runtime item id, then the legacy nested id, and |
| 423 | * mobile's `threadActivity`): an explicit `data.toolCallId` when the adapter | 442 | * finally the itemType/title/detail triple. Rows without any identity remain |
| 424 | * emits one, otherwise the itemType/title/detail triple. Returns null for rows | 443 | * untouched. |
| 425 | * with no identity at all — those never collapse on the client either, so they | ||
| 426 | * must not be dropped here. | ||
| 427 | */ | 444 | */ |
| 428 | function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string | null { | 445 | function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string | null { |
| 429 | const payload = asRecord(activity.payload); | 446 | const payload = asRecord(activity.payload); |
| ... | @@ -431,7 +448,8 @@ function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string | | ... | @@ -431,7 +448,8 @@ function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string | |
| 431 | return null; | 448 | return null; |
| 432 | } | 449 | } |
| 433 | 450 | ||
| 434 | const toolCallId = asTrimmedString(asRecord(payload.data)?.toolCallId); | 451 | const toolCallId = |
| 452 | asTrimmedString(payload.toolCallId) ?? asTrimmedString(asRecord(payload.data)?.toolCallId); | ||
| 435 | if (toolCallId) { | 453 | if (toolCallId) { |
| 436 | return `id:${toolCallId}`; | 454 | return `id:${toolCallId}`; |
| 437 | } | 455 | } |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+23-8| ... | @@ -2815,11 +2815,16 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -2815,11 +2815,16 @@ describe("ProviderRuntimeIngestion", () => { |
| 2815 | createdAt: now, | 2815 | createdAt: now, |
| 2816 | threadId: asThreadId("thread-1"), | 2816 | threadId: asThreadId("thread-1"), |
| 2817 | turnId: asTurnId("turn-9"), | 2817 | turnId: asTurnId("turn-9"), |
| 2818 | itemId: asItemId("tool-call-9"), | ||
| 2818 | payload: { | 2819 | payload: { |
| 2819 | itemType: "command_execution", | 2820 | itemType: "command_execution", |
| 2820 | status: "in_progress", | 2821 | status: "inProgress", |
| 2821 | title: "Read file", | 2822 | title: "Command run", |
| 2822 | detail: "/tmp/file.ts", | 2823 | detail: "Bash: vp test run", |
| 2824 | data: { | ||
| 2825 | toolName: "Bash", | ||
| 2826 | input: { command: "vp test run" }, | ||
| 2827 | }, | ||
| 2823 | }, | 2828 | }, |
| 2824 | }); | 2829 | }); |
| 2825 | 2830 | ||
| ... | @@ -2834,11 +2839,20 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -2834,11 +2839,20 @@ describe("ProviderRuntimeIngestion", () => { |
| 2834 | ); | 2839 | ); |
| 2835 | 2840 | ||
| 2836 | expect(thread.session?.status).toBe("ready"); | 2841 | expect(thread.session?.status).toBe("ready"); |
| 2837 | expect( | 2842 | const activity = thread.activities.find( |
| 2838 | thread.activities.some( | 2843 | (entry: ProviderRuntimeTestActivity) => entry.kind === "tool.started", |
| 2839 | (activity: ProviderRuntimeTestActivity) => activity.kind === "tool.started", | 2844 | ); |
| 2840 | ), | 2845 | const payload = activity?.payload as Record<string, unknown> | undefined; |
| 2841 | ).toBe(true); | 2846 | expect(payload).toMatchObject({ |
| 2847 | itemType: "command_execution", | ||
| 2848 | toolCallId: "tool-call-9", | ||
| 2849 | status: "inProgress", | ||
| 2850 | detail: "Bash: vp test run", | ||
| 2851 | data: { | ||
| 2852 | toolName: "Bash", | ||
| 2853 | input: { command: "vp test run" }, | ||
| 2854 | }, | ||
| 2855 | }); | ||
| 2842 | }); | 2856 | }); |
| 2843 | 2857 | ||
| 2844 | it("consumes P1 runtime events into thread metadata, diff checkpoints, and activities", async () => { | 2858 | it("consumes P1 runtime events into thread metadata, diff checkpoints, and activities", async () => { |
| ... | @@ -2956,6 +2970,7 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -2956,6 +2970,7 @@ describe("ProviderRuntimeIngestion", () => { |
| 2956 | expect(toolUpdate?.kind).toBe("tool.updated"); | 2970 | expect(toolUpdate?.kind).toBe("tool.updated"); |
| 2957 | expect(toolUpdatePayload?.itemType).toBe("command_execution"); | 2971 | expect(toolUpdatePayload?.itemType).toBe("command_execution"); |
| 2958 | expect(toolUpdatePayload?.status).toBe("in_progress"); | 2972 | expect(toolUpdatePayload?.status).toBe("in_progress"); |
| 2973 | expect(toolUpdatePayload?.toolCallId).toBe("item-p1-tool"); | ||
| 2959 | 2974 | ||
| 2960 | const warning = thread.activities.find( | 2975 | const warning = thread.activities.find( |
| 2961 | (activity: ProviderRuntimeTestActivity) => activity.id === "evt-runtime-warning", | 2976 | (activity: ProviderRuntimeTestActivity) => activity.id === "evt-runtime-warning", |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+6| ... | @@ -803,6 +803,7 @@ export function runtimeEventToActivities( | ... | @@ -803,6 +803,7 @@ export function runtimeEventToActivities( |
| 803 | summary: event.payload.title ?? "Tool updated", | 803 | summary: event.payload.title ?? "Tool updated", |
| 804 | payload: { | 804 | payload: { |
| 805 | itemType: event.payload.itemType, | 805 | itemType: event.payload.itemType, |
| 806 | ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), | ||
| 806 | ...(event.payload.status ? { status: event.payload.status } : {}), | 807 | ...(event.payload.status ? { status: event.payload.status } : {}), |
| 807 | ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), | 808 | ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), |
| 808 | ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), | 809 | ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), |
| ... | @@ -830,6 +831,8 @@ export function runtimeEventToActivities( | ... | @@ -830,6 +831,8 @@ export function runtimeEventToActivities( |
| 830 | summary: event.payload.title ?? "Tool", | 831 | summary: event.payload.title ?? "Tool", |
| 831 | payload: { | 832 | payload: { |
| 832 | itemType: event.payload.itemType, | 833 | itemType: event.payload.itemType, |
| 834 | ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), | ||
| 835 | ...(event.payload.status ? { status: event.payload.status } : {}), | ||
| 833 | ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), | 836 | ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), |
| 834 | ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), | 837 | ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), |
| 835 | ...(event.payload.agentId ? { agentId: event.payload.agentId } : {}), | 838 | ...(event.payload.agentId ? { agentId: event.payload.agentId } : {}), |
| ... | @@ -856,7 +859,10 @@ export function runtimeEventToActivities( | ... | @@ -856,7 +859,10 @@ export function runtimeEventToActivities( |
| 856 | summary: `${event.payload.title ?? "Tool"} started`, | 859 | summary: `${event.payload.title ?? "Tool"} started`, |
| 857 | payload: { | 860 | payload: { |
| 858 | itemType: event.payload.itemType, | 861 | itemType: event.payload.itemType, |
| 862 | ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), | ||
| 863 | ...(event.payload.status ? { status: event.payload.status } : {}), | ||
| 859 | ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), | 864 | ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}), |
| 865 | ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), | ||
| 860 | ...(event.payload.agentId ? { agentId: event.payload.agentId } : {}), | 866 | ...(event.payload.agentId ? { agentId: event.payload.agentId } : {}), |
| 861 | ...(event.payload.parentToolUseId | 867 | ...(event.payload.parentToolUseId |
| 862 | ? { parentToolUseId: event.payload.parentToolUseId } | 868 | ? { parentToolUseId: event.payload.parentToolUseId } |