Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 11 additions & 3 deletions apps/web/src/components/ThreadTerminalDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ResolvedKeybindingsConfig,
type ScopedThreadRef,
type TerminalEvent,
describeUnknownTerminalError,
terminalErrorMessage,
type TerminalSessionSnapshot,
type ThreadId,
Expand Down Expand Up @@ -703,12 +704,19 @@ export function TerminalViewport({
}
} catch (err) {
if (disposed) return;
// ru-fork: rebuild the real reason from the wire-decoded terminal error
// (the tagged error's `message` getter is lost over the RPC boundary),
// falling back to a plain Error message, then a generic string.
// ru-fork: dump the raw error first — when it isn't one of the 4 typed
// terminal tags (e.g. an RPC/transport/decode failure) both the
// formatter and `err.message` are empty, and the real reason was being
// discarded into a generic string.
console.error("[terminal] open failed", err);
// rebuild the real reason from the wire-decoded terminal error (the
// tagged error's `message` getter is lost over the RPC boundary),
// falling back to a plain Error message, then whatever the unknown wire
// object carries, then a generic string.
const detail =
terminalErrorMessage(err as { _tag?: string }) ??
(err instanceof Error ? err.message : undefined) ??
describeUnknownTerminalError(err) ??
"Не удалось открыть терминал.";
writeSystemMessage(terminal, detail);
}
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/rpc/wsTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ export class WsTransport {
this.lastHeartbeatPongAt = Date.now();
this.lifecycleHandlers?.onHeartbeatPong?.();
},
// ru-fork: a ping timeout surfaces as a Socket *Open* error, which
// effect's makeProtocolSocket swallows under retryTransientErrors — it
// silently reopens the socket WITHOUT replaying the subscription
// requests, so every live stream dies while unary calls (browse,
// dispatchCommand) keep working. Force our own session swap so the
// subscribe() loop sees session !== this.session and re-attaches every
// stream on the fresh socket.
onHeartbeatTimeout: () => {
this.lifecycleHandlers?.onHeartbeatTimeout?.();
void this.reconnect().catch(() => undefined);
},
}),
);
const clientScope = runtime.runSync(Scope.make());
Expand Down
20 changes: 20 additions & 0 deletions packages/contracts/src/ru-fork/terminalErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,23 @@ export function terminalErrorMessage(error: TerminalErrorShape): string | undefi
return undefined;
}
}

// ru-fork: last-resort detail for an error that is NOT a typed terminal error
// (RPC/transport failure, decode mismatch, a defect) — surface whatever the
// wire object carries instead of collapsing to a generic string.
export function describeUnknownTerminalError(error: unknown): string | undefined {
if (typeof error !== "object" || error === null) return undefined;
const shape = error as {
_tag?: string;
reason?: string;
message?: unknown;
cause?: unknown;
};
const tag = typeof shape._tag === "string" ? shape._tag : undefined;
const cause =
shape.cause && typeof shape.cause === "object" && "message" in shape.cause
? String((shape.cause as { message?: unknown }).message)
: undefined;
const parts = [tag, shape.reason, cause].filter(Boolean);
return parts.length > 0 ? `Терминал не открылся: ${parts.join(" · ")}` : undefined;
}
3 changes: 2 additions & 1 deletion patches/effect@4.0.0-beta.59.patch
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6
recievedPong = false;
- return writePing;
+ return (hooks?.onPing ?? Effect.void).pipe(Effect.andThen(writePing));
}).pipe(Effect.delay("5 seconds"), Effect.ignore, Effect.forever, Effect.interruptible, Effect.forkScoped);
- }).pipe(Effect.delay("5 seconds"), Effect.ignore, Effect.forever, Effect.interruptible, Effect.forkScoped);
+ }).pipe(Effect.delay("10 seconds"), Effect.ignore, Effect.forever, Effect.interruptible, Effect.forkScoped);
return {
timeout: latch.await,
@@ -773,6 +802,11 @@ export const makeProtocolWorker = options => Protocol.make(Effect.fnUntraced(fun
Expand Down
Loading