| ... | @@ -3449,10 +3449,14 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -3449,10 +3449,14 @@ describe("ClaudeAdapterLive", () => { |
| 3449 | const harness = makeHarness(); | 3449 | const harness = makeHarness(); |
| 3450 | return Effect.gen(function* () { | 3450 | return Effect.gen(function* () { |
| 3451 | const adapter = yield* ClaudeAdapter; | 3451 | const adapter = yield* ClaudeAdapter; |
| 3452 | const runtimeEvents: Array<ProviderRuntimeEvent> = []; | 3452 | const runtimeEventsFiber = yield* adapter.streamEvents.pipe( |
| 3453 | const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) => | 3453 | Stream.takeUntil( |
| 3454 | Effect.sync(() => runtimeEvents.push(event)), | 3454 | (event) => |
| 3455 | ).pipe(Effect.forkChild); | 3455 | event.type === "session.state.changed" && event.payload.reason === "api_retry:3/10", |
| | 3456 | ), |
| | 3457 | Stream.runCollect, |
| | 3458 | Effect.forkChild, |
| | 3459 | ); |
| 3456 | | 3460 | |
| 3457 | yield* adapter.startSession({ | 3461 | yield* adapter.startSession({ |
| 3458 | threadId: THREAD_ID, | 3462 | threadId: THREAD_ID, |
| ... | @@ -3498,7 +3502,6 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -3498,7 +3502,6 @@ describe("ClaudeAdapterLive", () => { |
| 3498 | uuid: "tu", | 3502 | uuid: "tu", |
| 3499 | }, | 3503 | }, |
| 3500 | { type: "system", subtype: "commands_changed", session_id: "session", uuid: "cc" }, | 3504 | { type: "system", subtype: "commands_changed", session_id: "session", uuid: "cc" }, |
| 3501 | { type: "system", subtype: "model_refusal_fallback", session_id: "session", uuid: "mrf" }, | | |
| 3502 | { type: "system", subtype: "local_command_output", session_id: "session", uuid: "lco" }, | 3505 | { type: "system", subtype: "local_command_output", session_id: "session", uuid: "lco" }, |
| 3503 | { type: "system", subtype: "plugin_install", session_id: "session", uuid: "pi" }, | 3506 | { type: "system", subtype: "plugin_install", session_id: "session", uuid: "pi" }, |
| 3504 | { type: "system", subtype: "memory_recall", session_id: "session", uuid: "mr" }, | 3507 | { type: "system", subtype: "memory_recall", session_id: "session", uuid: "mr" }, |
| ... | @@ -3545,6 +3548,21 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -3545,6 +3548,21 @@ describe("ClaudeAdapterLive", () => { |
| 3545 | ]) { | 3548 | ]) { |
| 3546 | harness.query.emit(message as unknown as SDKMessage); | 3549 | harness.query.emit(message as unknown as SDKMessage); |
| 3547 | } | 3550 | } |
| | 3551 | // Safety model-fallback notices DO surface as a warning row. |
| | 3552 | harness.query.emit({ |
| | 3553 | type: "system", |
| | 3554 | subtype: "model_refusal_fallback", |
| | 3555 | trigger: "refusal", |
| | 3556 | direction: "retry", |
| | 3557 | original_model: "claude-fable-5", |
| | 3558 | fallback_model: "claude-opus-4-8", |
| | 3559 | request_id: "req_test", |
| | 3560 | api_refusal_category: "cyber", |
| | 3561 | api_refusal_explanation: null, |
| | 3562 | content: "Safeguards flagged this message. Switched to Opus 4.8.", |
| | 3563 | session_id: "session", |
| | 3564 | uuid: "mrf", |
| | 3565 | } as unknown as SDKMessage); |
| 3548 | // High-priority notifications DO surface as a warning row. | 3566 | // High-priority notifications DO surface as a warning row. |
| 3549 | harness.query.emit({ | 3567 | harness.query.emit({ |
| 3550 | type: "system", | 3568 | type: "system", |
| ... | @@ -3602,15 +3620,15 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -3602,15 +3620,15 @@ describe("ClaudeAdapterLive", () => { |
| 3602 | session_id: "session", | 3620 | session_id: "session", |
| 3603 | uuid: "retry", | 3621 | uuid: "retry", |
| 3604 | } as unknown as SDKMessage); | 3622 | } as unknown as SDKMessage); |
| 3605 | yield* Effect.yieldNow; | 3623 | const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); |
| 3606 | yield* Effect.yieldNow; | | |
| 3607 | | 3624 | |
| 3608 | const warnings = runtimeEvents.filter((event) => event.type === "runtime.warning"); | 3625 | const warnings = runtimeEvents.filter((event) => event.type === "runtime.warning"); |
| 3609 | // Exactly three warnings: the high-priority notification, the | 3626 | // Exactly four warnings: the fallback notice, high-priority notification, |
| 3610 | // warning-level informational note, and the refusal. Nothing else. | 3627 | // warning-level informational note, and the refusal. Nothing else. |
| 3611 | assert.deepEqual( | 3628 | assert.deepEqual( |
| 3612 | warnings.map((event) => event.payload.message), | 3629 | warnings.map((event) => event.payload.message), |
| 3613 | [ | 3630 | [ |
| | 3631 | "Safeguards flagged this message. Switched to Opus 4.8.", |
| 3614 | "context window nearly full", | 3632 | "context window nearly full", |
| 3615 | "Stop hook prevented continuation", | 3633 | "Stop hook prevented continuation", |
| 3616 | "The request was declined by the API.", | 3634 | "The request was declined by the API.", |
| ... | @@ -3638,7 +3656,6 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -3638,7 +3656,6 @@ describe("ClaudeAdapterLive", () => { |
| 3638 | event.payload.reason.startsWith("api_retry:"), | 3656 | event.payload.reason.startsWith("api_retry:"), |
| 3639 | ); | 3657 | ); |
| 3640 | assert.equal(heartbeat?.type, "session.state.changed"); | 3658 | assert.equal(heartbeat?.type, "session.state.changed"); |
| 3641 | runtimeEventsFiber.interruptUnsafe(); | | |
| 3642 | }).pipe( | 3659 | }).pipe( |
| 3643 | Effect.provideService(Random.Random, makeDeterministicRandomService()), | 3660 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 3644 | Effect.provide(harness.layer), | 3661 | Effect.provide(harness.layer), |