@gtc6244/immediacy-daemon 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/src/crypto.d.ts +18 -39
  2. package/dist/src/crypto.js +33 -152
  3. package/dist/src/crypto.js.map +1 -1
  4. package/dist/src/devices.d.ts +46 -0
  5. package/dist/src/devices.js +114 -0
  6. package/dist/src/devices.js.map +1 -0
  7. package/dist/src/envelope.d.ts +3 -3
  8. package/dist/src/envelope.js +3 -3
  9. package/dist/src/forward.d.ts +4 -4
  10. package/dist/src/forward.js +1 -1
  11. package/dist/src/forward.js.map +1 -1
  12. package/dist/src/framing.d.ts +76 -0
  13. package/dist/src/framing.js +133 -0
  14. package/dist/src/framing.js.map +1 -0
  15. package/dist/src/hub.d.ts +29 -9
  16. package/dist/src/hub.js +20 -5
  17. package/dist/src/hub.js.map +1 -1
  18. package/dist/src/index.d.ts +3 -3
  19. package/dist/src/index.js +79 -17
  20. package/dist/src/index.js.map +1 -1
  21. package/dist/src/lan.d.ts +103 -0
  22. package/dist/src/lan.js +182 -0
  23. package/dist/src/lan.js.map +1 -0
  24. package/dist/src/pairing.d.ts +69 -0
  25. package/dist/src/pairing.js +125 -0
  26. package/dist/src/pairing.js.map +1 -0
  27. package/dist/src/protocol.d.ts +66 -23
  28. package/dist/src/protocol.js +4 -1
  29. package/dist/src/protocol.js.map +1 -1
  30. package/dist/src/push.d.ts +104 -0
  31. package/dist/src/push.js +339 -0
  32. package/dist/src/push.js.map +1 -0
  33. package/dist/src/relay-transport.d.ts +62 -0
  34. package/dist/src/relay-transport.js +180 -0
  35. package/dist/src/relay-transport.js.map +1 -0
  36. package/dist/src/relay-wire.d.ts +25 -0
  37. package/dist/src/relay-wire.js +37 -0
  38. package/dist/src/relay-wire.js.map +1 -0
  39. package/dist/src/rendezvous.d.ts +57 -0
  40. package/dist/src/rendezvous.js +152 -0
  41. package/dist/src/rendezvous.js.map +1 -0
  42. package/dist/src/server.d.ts +19 -7
  43. package/dist/src/server.js +180 -63
  44. package/dist/src/server.js.map +1 -1
  45. package/dist/src/session-socket.d.ts +34 -0
  46. package/dist/src/session-socket.js +84 -0
  47. package/dist/src/session-socket.js.map +1 -0
  48. package/dist/src/session.d.ts +151 -0
  49. package/dist/src/session.js +241 -0
  50. package/dist/src/session.js.map +1 -0
  51. package/dist/src/tcp-transport.d.ts +37 -0
  52. package/dist/src/tcp-transport.js +106 -0
  53. package/dist/src/tcp-transport.js.map +1 -0
  54. package/dist/src/telemetry.d.ts +69 -0
  55. package/dist/src/telemetry.js +64 -0
  56. package/dist/src/telemetry.js.map +1 -0
  57. package/dist/src/transport.d.ts +75 -0
  58. package/dist/src/transport.js +136 -0
  59. package/dist/src/transport.js.map +1 -0
  60. package/dist/src/workspace.d.ts +20 -5
  61. package/dist/src/workspace.js +19 -1
  62. package/dist/src/workspace.js.map +1 -1
  63. package/package.json +17 -4
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Transport telemetry (docs/WEBRTC_MIGRATION.md §5). Starts in Phase 4 because
3
+ * the numbers here drive two decisions: the DO free-tier budget, and whether
4
+ * Phase 6 (WebRTC) is worth building at all.
5
+ *
6
+ * A tiny in-memory recorder with a typed API + optional event sink. It records:
7
+ * - which transport each session used, and failover events;
8
+ * - time-to-connect per transport;
9
+ * - relay message count + byte volume (the DO billing driver);
10
+ * - handshake failures by cause;
11
+ * - reconnects, split by cause (mobile-backgrounding vs network change).
12
+ * No payloads, no identities — counts and durations only.
13
+ */
14
+ export type TransportKind = "loopback" | "lan" | "relay" | "webrtc";
15
+ export type HandshakeFailureCause = "unknown_device" | "bad_signature" | "expired_code" | "bad_message" | "timeout";
16
+ export type ReconnectCause = "mobile_background" | "network_change" | "daemon_restart" | "other";
17
+ interface ConnectStat {
18
+ count: number;
19
+ totalMs: number;
20
+ maxMs: number;
21
+ }
22
+ export interface TelemetrySnapshot {
23
+ sessionsByTransport: Record<string, number>;
24
+ failovers: number;
25
+ timeToConnectMs: Record<string, ConnectStat>;
26
+ relay: {
27
+ messages: number;
28
+ bytes: number;
29
+ };
30
+ handshakeFailures: Record<string, number>;
31
+ reconnects: Record<string, number>;
32
+ }
33
+ export type TelemetryEvent = {
34
+ kind: "session";
35
+ transport: TransportKind;
36
+ connectMs: number;
37
+ } | {
38
+ kind: "failover";
39
+ from: TransportKind;
40
+ to: TransportKind;
41
+ } | {
42
+ kind: "relay_message";
43
+ bytes: number;
44
+ } | {
45
+ kind: "handshake_failure";
46
+ cause: HandshakeFailureCause;
47
+ } | {
48
+ kind: "reconnect";
49
+ cause: ReconnectCause;
50
+ };
51
+ export declare class Telemetry {
52
+ private sessionsByTransport;
53
+ private failovers;
54
+ private timeToConnect;
55
+ private relayMessages;
56
+ private relayBytes;
57
+ private handshakeFailures;
58
+ private reconnects;
59
+ /** Optional sink for live streaming (e.g. to a metrics endpoint). */
60
+ onEvent?: (event: TelemetryEvent) => void;
61
+ private emit;
62
+ sessionEstablished(transport: TransportKind, connectMs: number): void;
63
+ failover(from: TransportKind, to: TransportKind): void;
64
+ relayMessage(bytes: number): void;
65
+ handshakeFailure(cause: HandshakeFailureCause): void;
66
+ reconnect(cause: ReconnectCause): void;
67
+ snapshot(): TelemetrySnapshot;
68
+ }
69
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Transport telemetry (docs/WEBRTC_MIGRATION.md §5). Starts in Phase 4 because
3
+ * the numbers here drive two decisions: the DO free-tier budget, and whether
4
+ * Phase 6 (WebRTC) is worth building at all.
5
+ *
6
+ * A tiny in-memory recorder with a typed API + optional event sink. It records:
7
+ * - which transport each session used, and failover events;
8
+ * - time-to-connect per transport;
9
+ * - relay message count + byte volume (the DO billing driver);
10
+ * - handshake failures by cause;
11
+ * - reconnects, split by cause (mobile-backgrounding vs network change).
12
+ * No payloads, no identities — counts and durations only.
13
+ */
14
+ export class Telemetry {
15
+ sessionsByTransport = {};
16
+ failovers = 0;
17
+ timeToConnect = {};
18
+ relayMessages = 0;
19
+ relayBytes = 0;
20
+ handshakeFailures = {};
21
+ reconnects = {};
22
+ /** Optional sink for live streaming (e.g. to a metrics endpoint). */
23
+ onEvent;
24
+ emit(event) {
25
+ this.onEvent?.(event);
26
+ }
27
+ sessionEstablished(transport, connectMs) {
28
+ this.sessionsByTransport[transport] = (this.sessionsByTransport[transport] ?? 0) + 1;
29
+ const s = this.timeToConnect[transport] ?? { count: 0, totalMs: 0, maxMs: 0 };
30
+ s.count++;
31
+ s.totalMs += connectMs;
32
+ s.maxMs = Math.max(s.maxMs, connectMs);
33
+ this.timeToConnect[transport] = s;
34
+ this.emit({ kind: "session", transport, connectMs });
35
+ }
36
+ failover(from, to) {
37
+ this.failovers++;
38
+ this.emit({ kind: "failover", from, to });
39
+ }
40
+ relayMessage(bytes) {
41
+ this.relayMessages++;
42
+ this.relayBytes += bytes;
43
+ this.emit({ kind: "relay_message", bytes });
44
+ }
45
+ handshakeFailure(cause) {
46
+ this.handshakeFailures[cause] = (this.handshakeFailures[cause] ?? 0) + 1;
47
+ this.emit({ kind: "handshake_failure", cause });
48
+ }
49
+ reconnect(cause) {
50
+ this.reconnects[cause] = (this.reconnects[cause] ?? 0) + 1;
51
+ this.emit({ kind: "reconnect", cause });
52
+ }
53
+ snapshot() {
54
+ return {
55
+ sessionsByTransport: { ...this.sessionsByTransport },
56
+ failovers: this.failovers,
57
+ timeToConnectMs: Object.fromEntries(Object.entries(this.timeToConnect).map(([k, v]) => [k, { ...v }])),
58
+ relay: { messages: this.relayMessages, bytes: this.relayBytes },
59
+ handshakeFailures: { ...this.handshakeFailures },
60
+ reconnects: { ...this.reconnects },
61
+ };
62
+ }
63
+ }
64
+ //# sourceMappingURL=telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/telemetry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Transport abstraction + channel mux (docs/WEBRTC_MIGRATION.md §3.7, Phase 2).
3
+ *
4
+ * `Transport` is the thin byte-pipe every path implements — loopback (this
5
+ * file), LAN TCP (Phase 3), the DO relay (Phase 4), and WebRTC (Phase 6). It
6
+ * moves opaque frame bytes and exposes a `bufferedAmount` so the sender can
7
+ * apply backpressure. `Mux` layers the frame codec (framing.ts) on top: it
8
+ * chunks large messages, multiplexes the control/rpc/terminal/events channels
9
+ * over the one pipe, reassembles on receipt, and pauses the sender above a
10
+ * high-water mark so a terminal firehose can't queue multi-second lag.
11
+ */
12
+ export interface Transport {
13
+ /** Send one already-encoded frame. */
14
+ send(bytes: Buffer): void;
15
+ /** Bytes handed to the transport but not yet flushed to the peer. */
16
+ bufferedAmount(): number;
17
+ /** Register the inbound-frame handler (one per transport). */
18
+ onFrame(cb: (bytes: Buffer) => void): void;
19
+ /** Register a close handler. */
20
+ onClose(cb: () => void): void;
21
+ /**
22
+ * Resolve once `bufferedAmount() < highWaterMark`. The sender awaits this
23
+ * between chunks to bound queue depth. Resolves immediately if already below.
24
+ */
25
+ whenWritable(highWaterMark: number): Promise<void>;
26
+ close(): void;
27
+ }
28
+ /** Default backpressure high-water mark (1 MiB queued). */
29
+ export declare const DEFAULT_HIGH_WATER: number;
30
+ /**
31
+ * An in-process, ordered, reliable transport pair for tests and same-process
32
+ * wiring. `pair()` returns two endpoints; a frame sent on one is delivered to
33
+ * the other on a later microtask (so `bufferedAmount` is observable and drains
34
+ * asynchronously, exercising the backpressure path).
35
+ */
36
+ export declare class LoopbackTransport implements Transport {
37
+ private peer;
38
+ private frameCb;
39
+ private closeCb;
40
+ private buffered;
41
+ private closed;
42
+ private drainWaiters;
43
+ static pair(): [LoopbackTransport, LoopbackTransport];
44
+ send(bytes: Buffer): void;
45
+ bufferedAmount(): number;
46
+ onFrame(cb: (bytes: Buffer) => void): void;
47
+ onClose(cb: () => void): void;
48
+ whenWritable(highWaterMark: number): Promise<void>;
49
+ private wakeDrainWaiters;
50
+ close(): void;
51
+ }
52
+ /**
53
+ * Channel multiplexer over a `Transport`. Chunks + reassembles messages, keeps a
54
+ * per-channel outgoing sequence, and applies backpressure between chunks.
55
+ */
56
+ export declare class Mux {
57
+ private readonly transport;
58
+ private readonly highWaterMark;
59
+ private readonly reassembler;
60
+ private readonly seqs;
61
+ private onMsg;
62
+ constructor(transport: Transport, opts?: {
63
+ highWaterMark?: number;
64
+ });
65
+ /** Register the reassembled-message handler. */
66
+ onMessage(cb: (channelId: number, message: Buffer) => void): void;
67
+ /** True while the transport buffer is below the high-water mark. */
68
+ writable(): boolean;
69
+ /**
70
+ * Send a logical message on `channelId`, chunked as needed. Resolves once all
71
+ * chunks are handed to the transport; awaits drain between chunks so a large
72
+ * message (or a terminal firehose) can't grow the queue without bound.
73
+ */
74
+ send(channelId: number, message: Buffer): Promise<void>;
75
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Transport abstraction + channel mux (docs/WEBRTC_MIGRATION.md §3.7, Phase 2).
3
+ *
4
+ * `Transport` is the thin byte-pipe every path implements — loopback (this
5
+ * file), LAN TCP (Phase 3), the DO relay (Phase 4), and WebRTC (Phase 6). It
6
+ * moves opaque frame bytes and exposes a `bufferedAmount` so the sender can
7
+ * apply backpressure. `Mux` layers the frame codec (framing.ts) on top: it
8
+ * chunks large messages, multiplexes the control/rpc/terminal/events channels
9
+ * over the one pipe, reassembles on receipt, and pauses the sender above a
10
+ * high-water mark so a terminal firehose can't queue multi-second lag.
11
+ */
12
+ import { decodeFrame, encodeFrame, splitMessage, Reassembler, } from "./framing.js";
13
+ /** Default backpressure high-water mark (1 MiB queued). */
14
+ export const DEFAULT_HIGH_WATER = 1024 * 1024;
15
+ /**
16
+ * An in-process, ordered, reliable transport pair for tests and same-process
17
+ * wiring. `pair()` returns two endpoints; a frame sent on one is delivered to
18
+ * the other on a later microtask (so `bufferedAmount` is observable and drains
19
+ * asynchronously, exercising the backpressure path).
20
+ */
21
+ export class LoopbackTransport {
22
+ peer;
23
+ frameCb = null;
24
+ closeCb = null;
25
+ buffered = 0;
26
+ closed = false;
27
+ drainWaiters = [];
28
+ static pair() {
29
+ const a = new LoopbackTransport();
30
+ const b = new LoopbackTransport();
31
+ a.peer = b;
32
+ b.peer = a;
33
+ return [a, b];
34
+ }
35
+ send(bytes) {
36
+ if (this.closed)
37
+ return;
38
+ const copy = Buffer.from(bytes);
39
+ this.buffered += copy.length;
40
+ // Deliver asynchronously, then decrement our buffer and wake drain waiters.
41
+ queueMicrotask(() => {
42
+ this.peer.frameCb?.(copy);
43
+ this.buffered -= copy.length;
44
+ this.wakeDrainWaiters();
45
+ });
46
+ }
47
+ bufferedAmount() {
48
+ return this.buffered;
49
+ }
50
+ onFrame(cb) {
51
+ this.frameCb = cb;
52
+ }
53
+ onClose(cb) {
54
+ this.closeCb = cb;
55
+ }
56
+ whenWritable(highWaterMark) {
57
+ if (this.buffered < highWaterMark)
58
+ return Promise.resolve();
59
+ return new Promise((resolve) => {
60
+ this.drainWaiters.push({ hw: highWaterMark, resolve });
61
+ });
62
+ }
63
+ wakeDrainWaiters() {
64
+ if (this.drainWaiters.length === 0)
65
+ return;
66
+ const still = [];
67
+ for (const w of this.drainWaiters) {
68
+ if (this.buffered < w.hw)
69
+ w.resolve();
70
+ else
71
+ still.push(w);
72
+ }
73
+ this.drainWaiters = still;
74
+ }
75
+ close() {
76
+ if (this.closed)
77
+ return;
78
+ this.closed = true;
79
+ for (const w of this.drainWaiters)
80
+ w.resolve();
81
+ this.drainWaiters = [];
82
+ queueMicrotask(() => this.peer.closeCb?.());
83
+ }
84
+ }
85
+ /**
86
+ * Channel multiplexer over a `Transport`. Chunks + reassembles messages, keeps a
87
+ * per-channel outgoing sequence, and applies backpressure between chunks.
88
+ */
89
+ export class Mux {
90
+ transport;
91
+ highWaterMark;
92
+ reassembler = new Reassembler();
93
+ seqs = new Map();
94
+ onMsg = null;
95
+ constructor(transport, opts) {
96
+ this.transport = transport;
97
+ this.highWaterMark = opts?.highWaterMark ?? DEFAULT_HIGH_WATER;
98
+ this.transport.onFrame((bytes) => {
99
+ let out;
100
+ try {
101
+ out = this.reassembler.push(decodeFrame(bytes));
102
+ }
103
+ catch {
104
+ return; // malformed/oversized frame — drop, keep the transport
105
+ }
106
+ if (out)
107
+ this.onMsg?.(out.channelId, out.message);
108
+ });
109
+ }
110
+ /** Register the reassembled-message handler. */
111
+ onMessage(cb) {
112
+ this.onMsg = cb;
113
+ }
114
+ /** True while the transport buffer is below the high-water mark. */
115
+ writable() {
116
+ return this.transport.bufferedAmount() < this.highWaterMark;
117
+ }
118
+ /**
119
+ * Send a logical message on `channelId`, chunked as needed. Resolves once all
120
+ * chunks are handed to the transport; awaits drain between chunks so a large
121
+ * message (or a terminal firehose) can't grow the queue without bound.
122
+ */
123
+ async send(channelId, message) {
124
+ let seq = this.seqs.get(channelId) ?? 0;
125
+ const frames = splitMessage(channelId, seq, message);
126
+ for (const frame of frames) {
127
+ this.transport.send(encodeFrame(frame));
128
+ seq = (frame.seq + 1) >>> 0;
129
+ if (this.transport.bufferedAmount() >= this.highWaterMark) {
130
+ await this.transport.whenWritable(this.highWaterMark);
131
+ }
132
+ }
133
+ this.seqs.set(channelId, seq);
134
+ }
135
+ }
136
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  * worktree, agent, and terminal. Translates agent events into broadcast wire
4
4
  * messages and persists metadata so sessions survive a daemon restart (resume).
5
5
  */
6
- import type { WebSocket } from "ws";
6
+ import type { SocketLike } from "./hub.js";
7
7
  import type { ChatEntry, DeleteBatchResultPayload, PromptAttachment, QuestionAnswers, WorkspaceActionKind, WorkspaceSummary } from "./protocol.js";
8
8
  import type { ProjectRecord, WorkspaceRecord } from "./types.js";
9
9
  import type { Hub } from "./hub.js";
@@ -14,6 +14,7 @@ import { ProjectStore } from "./projects.js";
14
14
  import { AppConfigStore } from "./config.js";
15
15
  import { CredentialsStore } from "./credentials.js";
16
16
  import { AllowlistStore } from "./allowlist.js";
17
+ import type { PushNotifier } from "./push.js";
17
18
  import type { PermissionResponse } from "./agent/agent.js";
18
19
  export declare class WorkspaceManager {
19
20
  private hub;
@@ -47,6 +48,13 @@ export declare class WorkspaceManager {
47
48
  * a permission request with `allow_all`. Defaulted for existing callers/tests.
48
49
  */
49
50
  private allowlist;
51
+ /**
52
+ * FCM remote-wake. Optional so existing callers/tests need no change; when
53
+ * present, the manager wakes phones on a permission request / agent-done /
54
+ * usage limit — but only when no client socket is live (the notifier gates
55
+ * on that), so a foregrounded app is never double-notified. See push.ts.
56
+ */
57
+ private push?;
50
58
  private workspaces;
51
59
  /**
52
60
  * The last workspace list we fanned out, JSON-encoded — so
@@ -83,7 +91,14 @@ export declare class WorkspaceManager {
83
91
  * grant auto-approves matching calls, and appended to when a client answers
84
92
  * a permission request with `allow_all`. Defaulted for existing callers/tests.
85
93
  */
86
- allowlist?: AllowlistStore);
94
+ allowlist?: AllowlistStore,
95
+ /**
96
+ * FCM remote-wake. Optional so existing callers/tests need no change; when
97
+ * present, the manager wakes phones on a permission request / agent-done /
98
+ * usage limit — but only when no client socket is live (the notifier gates
99
+ * on that), so a foregrounded app is never double-notified. See push.ts.
100
+ */
101
+ push?: PushNotifier | undefined);
87
102
  /** The daemon-global allowlist (for server-side admin: list/revoke/clear). */
88
103
  allowlistStore(): AllowlistStore;
89
104
  /** Is there a live workspace with this id? (used by the job scheduler). */
@@ -327,10 +342,10 @@ export declare class WorkspaceManager {
327
342
  * are intentionally left intact so their scrollback survives a reconnect.
328
343
  */
329
344
  reapDevice(deviceId: string): void;
330
- terminalInput(id: string, deviceId: string, data: string, socket: WebSocket): void;
331
- terminalResize(id: string, deviceId: string, cols: number, rows: number, socket: WebSocket): void;
345
+ terminalInput(id: string, deviceId: string, data: string, socket: SocketLike): void;
346
+ terminalResize(id: string, deviceId: string, cols: number, rows: number, socket: SocketLike): void;
332
347
  /** This device's retained PTY scrollback ("" until it has started one). */
333
- terminalSnapshot(id: string, deviceId: string, socket: WebSocket): string | null;
348
+ terminalSnapshot(id: string, deviceId: string, socket: SocketLike): string | null;
334
349
  gitStatus(id: string, sync?: boolean): Promise<import("./protocol.js").GitStatusResultPayload | null>;
335
350
  gitCommit(id: string, message: string): Promise<import("./protocol.js").GitResultPayload | null>;
336
351
  gitPush(id: string): Promise<import("./protocol.js").GitResultPayload | null>;
@@ -83,6 +83,7 @@ export class WorkspaceManager {
83
83
  credentials;
84
84
  appConfig;
85
85
  allowlist;
86
+ push;
86
87
  workspaces = new Map();
87
88
  /**
88
89
  * The last workspace list we fanned out, JSON-encoded — so
@@ -119,7 +120,14 @@ export class WorkspaceManager {
119
120
  * grant auto-approves matching calls, and appended to when a client answers
120
121
  * a permission request with `allow_all`. Defaulted for existing callers/tests.
121
122
  */
122
- allowlist = new AllowlistStore()) {
123
+ allowlist = new AllowlistStore(),
124
+ /**
125
+ * FCM remote-wake. Optional so existing callers/tests need no change; when
126
+ * present, the manager wakes phones on a permission request / agent-done /
127
+ * usage limit — but only when no client socket is live (the notifier gates
128
+ * on that), so a foregrounded app is never double-notified. See push.ts.
129
+ */
130
+ push) {
123
131
  this.hub = hub;
124
132
  this.chatlog = chatlog;
125
133
  this.media = media;
@@ -128,6 +136,7 @@ export class WorkspaceManager {
128
136
  this.credentials = credentials;
129
137
  this.appConfig = appConfig;
130
138
  this.allowlist = allowlist;
139
+ this.push = push;
131
140
  }
132
141
  /** The daemon-global allowlist (for server-side admin: list/revoke/clear). */
133
142
  allowlistStore() {
@@ -1263,6 +1272,9 @@ export class WorkspaceManager {
1263
1272
  inputSummary: e.inputSummary,
1264
1273
  seq: permEntry.seq,
1265
1274
  });
1275
+ // Remote-wake a backgrounded phone so the deferred call doesn't sit
1276
+ // unseen (no-op when a client socket is live — see PushNotifier).
1277
+ this.push?.notifyPermission(id, live.record.name, e.tool, e.inputSummary, permEntry.seq);
1266
1278
  break;
1267
1279
  }
1268
1280
  case "permission_resolved":
@@ -1327,6 +1339,9 @@ export class WorkspaceManager {
1327
1339
  usageText,
1328
1340
  seq: doneEntry.seq,
1329
1341
  });
1342
+ // Remote-wake so a turn that finished while the app was closed still
1343
+ // notifies (no-op when a client socket is live — see PushNotifier).
1344
+ this.push?.notifyDone(id, live.record.name, e.summary, doneEntry.seq);
1330
1345
  // The turn almost certainly touched files, so the sidebar diff-stat is
1331
1346
  // now stale — recompute and fan the fresh counts out (no-ops if the turn
1332
1347
  // changed nothing). This keeps a background workspace's counter live
@@ -1349,6 +1364,9 @@ export class WorkspaceManager {
1349
1364
  message: e.message,
1350
1365
  resetAt: e.resetAt,
1351
1366
  });
1367
+ // Remote-wake so the user learns the run stalled on a limit even with the
1368
+ // app closed (no-op when a client socket is live — see PushNotifier).
1369
+ this.push?.notifyLimit(id, live.record.name, e.message);
1352
1370
  // Persist as a `limit` entry (not a plain system line) so a reconnecting
1353
1371
  // client replays the actionable restart card, not just static text.
1354
1372
  void this.chatlog.append(id, {