pinomad 0.12.0 → 0.14.0

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 (47) hide show
  1. package/dist/chunk-0dh2k3er.js +43 -0
  2. package/dist/{chunk-47ff1d1t.js → chunk-1mjbt2z2.js} +191 -11
  3. package/dist/{chunk-7wj0jh3t.js → chunk-1rdbnvy6.js} +1 -1
  4. package/dist/{chunk-ksfxqd8s.js → chunk-35szq73k.js} +2682 -459
  5. package/dist/{chunk-mnmqyq7d.js → chunk-5j7ybhjj.js} +1984 -333
  6. package/dist/{chunk-agq4tn9r.js → chunk-5pv0g1nj.js} +12 -10
  7. package/dist/{chunk-7mn38n4f.js → chunk-63ymn5c9.js} +2 -2
  8. package/dist/{chunk-nppgxh6c.js → chunk-6j6hkfbj.js} +8 -4
  9. package/dist/{chunk-pqe7q1s7.js → chunk-6vbp1vva.js} +2 -1
  10. package/dist/{chunk-g3ahp7b9.js → chunk-9trbbz4r.js} +1 -1
  11. package/dist/{chunk-gg2dcqbg.js → chunk-b0nwq1dt.js} +29 -12
  12. package/dist/chunk-dsr19q5h.js +23 -0
  13. package/dist/{chunk-41q0saz0.js → chunk-e53c0s8z.js} +9 -7
  14. package/dist/{chunk-np4jd070.js → chunk-e59k3bmz.js} +307 -56
  15. package/dist/chunk-eerpgvy9.js +315 -0
  16. package/dist/chunk-eewxy4cp.js +240 -0
  17. package/dist/{chunk-vqkb70d7.js → chunk-evbv1fkn.js} +3 -3
  18. package/dist/{chunk-vx7dy07y.js → chunk-ex5yr09h.js} +11 -6
  19. package/dist/{chunk-t91zt5f0.js → chunk-hg9gs396.js} +1 -1
  20. package/dist/{chunk-aqj9wfmb.js → chunk-mtkvft88.js} +1 -1
  21. package/dist/{chunk-t5abqsva.js → chunk-mxwcnnkg.js} +15 -305
  22. package/dist/{chunk-5qf1grkk.js → chunk-w927q9m1.js} +52 -16
  23. package/dist/pinomad.js +12 -12
  24. package/package.json +16 -11
  25. package/web/assets/{core-DnpzcQzs.js → core-BWHGsFpV.js} +1 -1
  26. package/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  27. package/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  28. package/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  29. package/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  30. package/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  31. package/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  32. package/web/assets/{highlighted-diff-Dn5ifsvK.js → highlighted-diff-DJ7KZCpS.js} +2 -2
  33. package/web/assets/{highlights-DnHv6NBM.js → highlights-CyEmo6OI.js} +1 -1
  34. package/web/assets/{highlights-themes-CUdrlZk3.js → highlights-themes-CSNkBojy.js} +1 -1
  35. package/web/assets/index-C_qkbyL7.css +1 -0
  36. package/web/assets/index-DefQtCI2.js +396 -0
  37. package/web/assets/{shiki-BuPP5HCg.js → shiki-BzCmRG-v.js} +2 -2
  38. package/web/assets/{shiki-langs-FJk0v0Ga.js → shiki-langs-DOkjyV4d.js} +1 -1
  39. package/web/assets/{stream-tokenizer-Bl6bisIa.js → stream-tokenizer-CBPqFe4Z.js} +1 -1
  40. package/web/assets/{theming-themes-BL_CC-Mm.js → theming-themes-wKVca0aB.js} +1 -1
  41. package/web/assets/{xterm-view-ZsuzJb2C.js → xterm-view-CKa1-QCM.js} +12 -12
  42. package/web/index.html +3 -2
  43. package/web/theme-boot.js +11 -0
  44. package/dist/chunk-42nqtx5r.js +0 -22
  45. package/dist/chunk-jeqsdcj1.js +0 -96
  46. package/web/assets/index-BV0ecolo.css +0 -1
  47. package/web/assets/index-DNsTTgPE.js +0 -396
@@ -3,19 +3,26 @@ import {
3
3
  MAX_ATTACHMENTS_PER_MESSAGE,
4
4
  PROTOCOL,
5
5
  RTC_DATA_CHANNEL_LABEL,
6
+ TRANSCRIPT_PAGE,
6
7
  UNAUTHORIZED_CLOSE_CODE,
8
+ addEarlier,
7
9
  checkoutOf,
10
+ classify,
8
11
  conversationStream,
9
12
  docStream,
10
13
  homeOf,
11
14
  hostStreams,
12
15
  hostSupportsCall,
13
16
  hostSupportsFeature,
17
+ isBusy,
18
+ isProcessesHello,
14
19
  isRtcHello,
15
20
  organize,
16
21
  parseRtcMaxMessageSize,
17
- rtcSelectedPair
18
- } from "./chunk-47ff1d1t.js";
22
+ processStream,
23
+ rtcSelectedPair,
24
+ withEarlier
25
+ } from "./chunk-1mjbt2z2.js";
19
26
  import {
20
27
  SECURE_PROLOGUE,
21
28
  SecureSession,
@@ -24,7 +31,7 @@ import {
24
31
  initiateIK,
25
32
  isLoopbackUrl,
26
33
  parseEndpointSet
27
- } from "./chunk-pqe7q1s7.js";
34
+ } from "./chunk-6vbp1vva.js";
28
35
 
29
36
  // packages/protocol/src/remote-durable.ts
30
37
  import { applyImmutable } from "@earendil-works/chord/delta";
@@ -293,12 +300,6 @@ function isTerminalsHello(value) {
293
300
  var terminalsStream = (id) => `terminals:${id}`;
294
301
  var terminalStream = (id) => `terminal:${id}`;
295
302
 
296
- // packages/protocol/src/transcript.ts
297
- function isBusy(view) {
298
- const live = view.docs?.["pi.live"];
299
- return live?.run !== undefined;
300
- }
301
-
302
303
  // packages/protocol/src/remote-durable.ts
303
304
  var DEFAULT_CANDIDATE_TIMEOUTS = {
304
305
  socketOpenMs: 5000,
@@ -396,6 +397,8 @@ class RemoteClient {
396
397
  #options;
397
398
  #listeners = new Set;
398
399
  #values = new Map;
400
+ #earlier = new Map;
401
+ #loadingEarlier = new Map;
399
402
  #wanted = new Set;
400
403
  #fresh = new Set;
401
404
  #docs = [];
@@ -454,6 +457,7 @@ class RemoteClient {
454
457
  #state;
455
458
  #current;
456
459
  #side;
460
+ #acquiring = new Map;
457
461
  #nextCall = 1;
458
462
  #nextNotice = 1;
459
463
  #submissions = new Map;
@@ -466,10 +470,14 @@ class RemoteClient {
466
470
  #helloDefaults = {};
467
471
  #hostVersion;
468
472
  #hostTerminals;
473
+ #hostProcesses;
469
474
  #terminalWatchers = new Map;
470
475
  #terminalSinks = new Map;
471
476
  #terminalPositions = new Map;
472
477
  #terminalSizes = new Map;
478
+ #processWatchers = new Map;
479
+ #processSinks = new Map;
480
+ #processPositions = new Map;
473
481
  #attempt = 0;
474
482
  #rtcAttemptNonce = randomHex(8);
475
483
  #closed = false;
@@ -723,6 +731,7 @@ class RemoteClient {
723
731
  this.#helloDefaults = frame.defaults ?? {};
724
732
  this.#hostVersion = frame.hostVersion;
725
733
  this.#hostTerminals = isTerminalsHello(frame.terminals) ? frame.terminals : undefined;
734
+ this.#hostProcesses = isProcessesHello(frame.processes) ? frame.processes : undefined;
726
735
  this.#fresh.clear();
727
736
  const covered = new Set(hostStreams(hostMinor));
728
737
  for (const name of HOST_STREAM_NAMES) {
@@ -741,6 +750,7 @@ class RemoteClient {
741
750
  for (const stream of this.#conversationStreams(this.#side))
742
751
  this.#wanted.add(stream);
743
752
  this.#terminalsAvailability();
753
+ this.#processesAvailability();
744
754
  for (const stream of this.#wanted)
745
755
  this.#subscribe(stream);
746
756
  if (frame.endpointSet !== undefined) {
@@ -823,6 +833,7 @@ class RemoteClient {
823
833
  providers: null,
824
834
  settings: null,
825
835
  agents: null,
836
+ upgrade: null,
826
837
  host: this.#hostInfo()
827
838
  };
828
839
  this.#refresh();
@@ -830,6 +841,7 @@ class RemoteClient {
830
841
  view: this.#viewSource(),
831
842
  controller: this.#controller(),
832
843
  terminals: this.#terminals(),
844
+ processes: this.#processes(),
833
845
  setConnectionMode: (mode) => this.setConnectionMode(mode),
834
846
  rtcDiagnostics: () => this.rtcDiagnostics(),
835
847
  close: () => this.#close()
@@ -1985,9 +1997,13 @@ class RemoteClient {
1985
1997
  return;
1986
1998
  }
1987
1999
  this.#values.set(frame.stream, frame.value);
2000
+ this.#earlier.delete(frame.stream);
2001
+ this.#loadingEarlier.delete(frame.stream);
1988
2002
  this.#fresh.add(frame.stream);
1989
2003
  this.#streamVersions.set(frame.stream, (this.#streamVersions.get(frame.stream) ?? 0) + 1);
1990
2004
  this.#refresh();
2005
+ if (this.#processWatchers.has(frame.stream))
2006
+ this.#notifyProcesses(frame.stream);
1991
2007
  for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? [])
1992
2008
  waiter.resolve();
1993
2009
  return;
@@ -1996,12 +2012,18 @@ class RemoteClient {
1996
2012
  return;
1997
2013
  this.#values.set(frame.stream, applyImmutable(this.#values.get(frame.stream), frame.ops));
1998
2014
  this.#refresh();
2015
+ if (this.#processWatchers.has(frame.stream))
2016
+ this.#notifyProcesses(frame.stream);
1999
2017
  return;
2000
2018
  case "output": {
2001
- const sink = this.#terminalSinks.get(frame.stream);
2019
+ const processSink = this.#processSinks.get(frame.stream);
2020
+ const sink = processSink ?? this.#terminalSinks.get(frame.stream);
2002
2021
  if (sink === undefined || !this.#wanted.has(frame.stream))
2003
2022
  return;
2004
- this.#terminalPositions.set(frame.stream, frame.seq);
2023
+ if (processSink === undefined)
2024
+ this.#terminalPositions.set(frame.stream, frame.seq);
2025
+ else
2026
+ this.#processPositions.set(frame.stream, frame.seq);
2005
2027
  sink.output(frame.data, frame.reset === true);
2006
2028
  return;
2007
2029
  }
@@ -2010,8 +2032,14 @@ class RemoteClient {
2010
2032
  this.#endTerminalStream(frame.stream, frame.reason);
2011
2033
  return;
2012
2034
  }
2035
+ if (frame.stream.startsWith("process:")) {
2036
+ this.#endProcessStream(frame.stream, frame.reason);
2037
+ return;
2038
+ }
2013
2039
  this.#wanted.delete(frame.stream);
2014
2040
  this.#values.delete(frame.stream);
2041
+ this.#earlier.delete(frame.stream);
2042
+ this.#loadingEarlier.delete(frame.stream);
2015
2043
  this.#fresh.delete(frame.stream);
2016
2044
  const ended = new Error(`Stream ${frame.stream} ended: ${frame.reason}`);
2017
2045
  for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? []) {
@@ -2026,6 +2054,8 @@ class RemoteClient {
2026
2054
  this.#side = undefined;
2027
2055
  }
2028
2056
  this.#refresh();
2057
+ if (this.#processWatchers.has(frame.stream))
2058
+ this.#notifyProcesses(frame.stream);
2029
2059
  return;
2030
2060
  }
2031
2061
  case "result": {
@@ -2079,40 +2109,100 @@ class RemoteClient {
2079
2109
  return [conversationStream(id), ...this.#docs.map((doc) => docStream(doc.kind, id))];
2080
2110
  }
2081
2111
  async#acquire(id) {
2082
- const missing = this.#conversationStreams(id).filter((stream) => !this.#wanted.has(stream));
2083
- const stale = this.#conversationStreams(id).filter((stream) => this.#wanted.has(stream) && !this.#fresh.has(stream));
2084
- for (const stream of missing)
2085
- this.#wanted.add(stream);
2086
- const shown = Promise.all([...missing, ...stale].map((stream) => this.#snapshot(stream)));
2087
- for (const stream of missing) {
2088
- if (this.#helloReceived)
2089
- this.#subscribe(stream);
2090
- }
2091
- await shown;
2092
- }
2093
- #release(id, other) {
2094
- const keep = new Set(other === undefined ? [] : this.#conversationStreams(other));
2112
+ this.#acquiring.set(id, (this.#acquiring.get(id) ?? 0) + 1);
2113
+ try {
2114
+ const missing = this.#conversationStreams(id).filter((stream) => !this.#wanted.has(stream));
2115
+ const stale = this.#conversationStreams(id).filter((stream) => this.#wanted.has(stream) && !this.#fresh.has(stream));
2116
+ for (const stream of missing)
2117
+ this.#wanted.add(stream);
2118
+ const shown = Promise.all([...missing, ...stale].map((stream) => this.#snapshot(stream)));
2119
+ for (const stream of missing) {
2120
+ if (this.#helloReceived)
2121
+ this.#subscribe(stream);
2122
+ }
2123
+ await shown;
2124
+ } finally {
2125
+ const left = (this.#acquiring.get(id) ?? 1) - 1;
2126
+ if (left > 0)
2127
+ this.#acquiring.set(id, left);
2128
+ else
2129
+ this.#acquiring.delete(id);
2130
+ }
2131
+ }
2132
+ #streamStillWanted(stream) {
2133
+ if (this.#processWatchers.has(stream))
2134
+ return true;
2135
+ for (const id of [this.#current, this.#side, ...this.#acquiring.keys()]) {
2136
+ if (id !== undefined && this.#conversationStreams(id).includes(stream))
2137
+ return true;
2138
+ }
2139
+ return false;
2140
+ }
2141
+ #releaseStream(stream) {
2142
+ this.#wanted.delete(stream);
2143
+ this.#values.delete(stream);
2144
+ this.#earlier.delete(stream);
2145
+ this.#loadingEarlier.delete(stream);
2146
+ this.#fresh.delete(stream);
2147
+ for (const waiter of this.#snapshotWaiters.get(stream)?.splice(0) ?? [])
2148
+ waiter.resolve();
2149
+ this.#send({ type: "unsubscribe", stream });
2150
+ }
2151
+ #release(id) {
2095
2152
  for (const stream of this.#conversationStreams(id)) {
2096
- if (keep.has(stream))
2153
+ if (this.#streamStillWanted(stream))
2097
2154
  continue;
2098
- this.#wanted.delete(stream);
2099
- this.#values.delete(stream);
2100
- this.#fresh.delete(stream);
2101
- for (const waiter of this.#snapshotWaiters.get(stream)?.splice(0) ?? [])
2102
- waiter.resolve();
2103
- this.#send({ type: "unsubscribe", stream });
2155
+ this.#releaseStream(stream);
2104
2156
  }
2105
2157
  }
2106
2158
  #subscribe(stream) {
2107
2159
  const refs = stream.startsWith("conversation:") && hostSupportsFeature(this.#hostMinor, "imageRefs");
2108
- const since = this.#terminalPositions.get(stream);
2160
+ const since = stream.startsWith("process:") ? this.#processPositions.get(stream) : this.#terminalPositions.get(stream);
2161
+ const window = this.#window(stream);
2109
2162
  this.#send({
2110
2163
  type: "subscribe",
2111
2164
  stream,
2112
2165
  ...refs ? { images: "ref" } : {},
2113
- ...since === undefined ? {} : { since }
2166
+ ...since === undefined ? {} : { since },
2167
+ ...window === undefined ? {} : { window }
2114
2168
  });
2115
2169
  }
2170
+ #window(stream) {
2171
+ if (!stream.startsWith("conversation:") || !hostSupportsFeature(this.#hostMinor, "transcriptPaging"))
2172
+ return;
2173
+ const from = this.#shownView(stream)?.entries[0]?.id;
2174
+ return from === undefined ? { limit: TRANSCRIPT_PAGE } : { limit: TRANSCRIPT_PAGE, from };
2175
+ }
2176
+ #shownView(stream) {
2177
+ const view = this.#values.get(stream);
2178
+ return view === undefined ? undefined : withEarlier(view, this.#earlier.get(stream));
2179
+ }
2180
+ #loadEarlier(id) {
2181
+ const stream = conversationStream(id);
2182
+ const inFlight = this.#loadingEarlier.get(stream);
2183
+ if (inFlight !== undefined)
2184
+ return inFlight;
2185
+ const requested = this.#shownView(stream)?.before;
2186
+ if (requested === undefined)
2187
+ return Promise.resolve();
2188
+ const load = this.#call("earlierEntries", { conversationId: id, before: requested, limit: TRANSCRIPT_PAGE }).then((page) => {
2189
+ if (this.#loadingEarlier.get(stream) !== load)
2190
+ return;
2191
+ const shown = this.#shownView(stream);
2192
+ if (shown === undefined)
2193
+ return;
2194
+ const earlier = addEarlier(shown, this.#earlier.get(stream), requested, page);
2195
+ if (earlier === undefined)
2196
+ return;
2197
+ this.#earlier.set(stream, earlier);
2198
+ this.#refresh();
2199
+ }).finally(() => {
2200
+ if (this.#loadingEarlier.get(stream) === load)
2201
+ this.#loadingEarlier.delete(stream);
2202
+ });
2203
+ this.#loadingEarlier.set(stream, load);
2204
+ return load;
2205
+ }
2116
2206
  #terminalsUnavailable() {
2117
2207
  if (!hostSupportsFeature(this.#hostMinor, "terminals")) {
2118
2208
  return "The host is too old for terminals (needs protocol 5.27); run `pinomad upgrade` on the host";
@@ -2224,6 +2314,122 @@ class RemoteClient {
2224
2314
  close: (terminalId) => this.#terminalCall("closeTerminal", { terminalId }).then(() => {})
2225
2315
  };
2226
2316
  }
2317
+ #processesUnavailable() {
2318
+ if (!hostSupportsFeature(this.#hostMinor, "processes")) {
2319
+ return "The host is too old for background processes (needs protocol 5.32); run `pinomad upgrade` on the host";
2320
+ }
2321
+ if (this.#hostProcesses === undefined)
2322
+ return "The host couldn't load its process module; its log says which package is missing";
2323
+ return;
2324
+ }
2325
+ #processDocDeclared() {
2326
+ return this.#docs.some((doc) => doc.kind === "pinomad.processes");
2327
+ }
2328
+ #processesAvailability() {
2329
+ const listable = hostSupportsFeature(this.#hostMinor, "processes") && this.#processDocDeclared();
2330
+ for (const stream of [...this.#processWatchers.keys()]) {
2331
+ if (listable) {
2332
+ this.#wanted.add(stream);
2333
+ } else {
2334
+ this.#wanted.delete(stream);
2335
+ this.#values.delete(stream);
2336
+ for (const waiter of this.#snapshotWaiters.get(stream)?.splice(0) ?? [])
2337
+ waiter.resolve();
2338
+ }
2339
+ this.#notifyProcesses(stream);
2340
+ }
2341
+ const reason = this.#processesUnavailable();
2342
+ if (reason === undefined) {
2343
+ for (const stream of this.#processSinks.keys())
2344
+ this.#wanted.add(stream);
2345
+ return;
2346
+ }
2347
+ for (const stream of [...this.#processSinks.keys()])
2348
+ this.#endProcessStream(stream, reason);
2349
+ }
2350
+ #notifyProcesses(stream) {
2351
+ const state = this.#processState(stream);
2352
+ for (const listener of [...this.#processWatchers.get(stream) ?? []])
2353
+ listener(state);
2354
+ }
2355
+ #processState(stream) {
2356
+ const value = this.#values.get(stream);
2357
+ const presentation = this.#docs.find((doc) => doc.kind === "pinomad.processes")?.presentation ?? "pinomad.processes";
2358
+ const classified = classify(presentation, value);
2359
+ const processes = classified.type === "pinomad.processes" ? classified.value.processes : [];
2360
+ const reason = this.#processesUnavailable();
2361
+ return {
2362
+ processes: [...processes],
2363
+ ...reason === undefined ? {} : { unavailable: reason }
2364
+ };
2365
+ }
2366
+ #endProcessStream(stream, reason) {
2367
+ this.#wanted.delete(stream);
2368
+ const sink = this.#processSinks.get(stream);
2369
+ this.#processSinks.delete(stream);
2370
+ this.#processPositions.delete(stream);
2371
+ sink?.ended(reason);
2372
+ }
2373
+ #processCall(method, args) {
2374
+ const reason = this.#processesUnavailable();
2375
+ return reason === undefined ? this.#call(method, args) : Promise.reject(new Error(reason));
2376
+ }
2377
+ #processes() {
2378
+ return {
2379
+ watch: (conversationId, listener) => {
2380
+ const stream = docStream("pinomad.processes", conversationId);
2381
+ let listeners = this.#processWatchers.get(stream);
2382
+ const first = listeners === undefined;
2383
+ if (listeners === undefined)
2384
+ this.#processWatchers.set(stream, listeners = new Set);
2385
+ listeners.add(listener);
2386
+ listener(this.#processState(stream));
2387
+ if (first && hostSupportsFeature(this.#hostMinor, "processes") && this.#processDocDeclared()) {
2388
+ this.#wanted.add(stream);
2389
+ if (this.#helloReceived)
2390
+ this.#subscribe(stream);
2391
+ }
2392
+ return () => {
2393
+ if (!listeners.delete(listener) || listeners.size > 0)
2394
+ return;
2395
+ this.#processWatchers.delete(stream);
2396
+ if (this.#streamStillWanted(stream))
2397
+ return;
2398
+ if (!this.#wanted.has(stream))
2399
+ return;
2400
+ this.#releaseStream(stream);
2401
+ };
2402
+ },
2403
+ attach: (processId, since, sink) => {
2404
+ const stream = processStream(processId);
2405
+ const reason = this.#processesUnavailable();
2406
+ if (reason !== undefined) {
2407
+ sink.ended(reason);
2408
+ return () => {};
2409
+ }
2410
+ if (this.#processSinks.has(stream) && this.#wanted.has(stream))
2411
+ this.#send({ type: "unsubscribe", stream });
2412
+ this.#processSinks.set(stream, sink);
2413
+ if (since === undefined)
2414
+ this.#processPositions.delete(stream);
2415
+ else
2416
+ this.#processPositions.set(stream, since);
2417
+ this.#wanted.add(stream);
2418
+ if (this.#helloReceived)
2419
+ this.#subscribe(stream);
2420
+ return () => {
2421
+ if (this.#processSinks.get(stream) !== sink)
2422
+ return;
2423
+ this.#processSinks.delete(stream);
2424
+ this.#processPositions.delete(stream);
2425
+ if (this.#wanted.delete(stream))
2426
+ this.#send({ type: "unsubscribe", stream });
2427
+ };
2428
+ },
2429
+ stop: (conversationId, processId) => this.#processCall("stopProcess", { conversationId, id: processId }).then(({ state }) => state),
2430
+ unavailable: () => this.#processesUnavailable()
2431
+ };
2432
+ }
2227
2433
  #host(name) {
2228
2434
  return this.#values.get(name);
2229
2435
  }
@@ -2324,22 +2530,33 @@ class RemoteClient {
2324
2530
  record.error = undefined;
2325
2531
  this.#clearSubmissionTimer(record);
2326
2532
  }
2327
- #entriesOf(id) {
2328
- if (id === undefined)
2329
- return [];
2330
- const view = this.#values.get(conversationStream(id));
2331
- return view?.entries ?? [];
2332
- }
2333
2533
  #entryPresent(record) {
2334
- if (record.entry === undefined)
2534
+ if (record.entry === undefined || record.conversationId === undefined)
2535
+ return false;
2536
+ const shown = this.#shownView(conversationStream(record.conversationId));
2537
+ if (shown === undefined)
2335
2538
  return false;
2336
- return this.#entriesOf(record.conversationId).some((entry) => String(entry.id) === String(record.entry));
2539
+ if (shown.before !== undefined && Number(record.entry) <= Number(shown.before))
2540
+ return true;
2541
+ return shown.entries.some((entry) => String(entry.id) === String(record.entry));
2542
+ }
2543
+ #inboxOf(id) {
2544
+ if (id === undefined)
2545
+ return;
2546
+ const view = this.#values.get(conversationStream(id));
2547
+ return view?.docs?.["pi.inbox"];
2337
2548
  }
2338
2549
  #inboxHas(record) {
2339
- if (record.conversationId === undefined || record.submissionId === undefined)
2550
+ if (record.submissionId === undefined)
2340
2551
  return false;
2341
- const inbox = this.#values.get(docStream("pi.inbox", record.conversationId));
2342
- return (inbox?.items ?? []).some((item) => String(item.id) === String(record.submissionId));
2552
+ const items = this.#inboxOf(record.conversationId)?.items ?? [];
2553
+ return items.some((item) => String(item.id) === String(record.submissionId));
2554
+ }
2555
+ #markChecked(record) {
2556
+ if (record.conversationId === undefined)
2557
+ return;
2558
+ record.checkedVersion = this.#streamVersions.get(conversationStream(record.conversationId)) ?? 0;
2559
+ record.checkedInbox = this.#inboxOf(record.conversationId);
2343
2560
  }
2344
2561
  #applySubmissionRecord(record, found) {
2345
2562
  record.admitted = true;
@@ -2370,11 +2587,10 @@ class RemoteClient {
2370
2587
  this.#settleSubmission(record);
2371
2588
  continue;
2372
2589
  }
2373
- if (record.conversationId !== undefined && record.submissionId !== undefined && record.admitted && (record.entry !== undefined || !this.#inboxHas(record))) {
2590
+ if (record.conversationId !== undefined && !record.confirming && record.admitted && (record.entry !== undefined || !this.#inboxHas(record))) {
2374
2591
  const stream = conversationStream(record.conversationId);
2375
- const version = (this.#streamVersions.get(stream) ?? 0) + (this.#streamVersions.get(docStream("pi.inbox", record.conversationId)) ?? 0);
2376
- if (this.#fresh.has(stream) && record.checkedVersion !== version) {
2377
- record.checkedVersion = version;
2592
+ if (this.#fresh.has(stream) && (record.checkedVersion !== (this.#streamVersions.get(stream) ?? 0) || record.checkedInbox !== this.#inboxOf(record.conversationId))) {
2593
+ this.#markChecked(record);
2378
2594
  this.#confirmSubmission(record);
2379
2595
  }
2380
2596
  }
@@ -2408,6 +2624,8 @@ class RemoteClient {
2408
2624
  } catch {} finally {
2409
2625
  record.confirming = false;
2410
2626
  }
2627
+ if (!this.#closed && !this.#submissionSettled(record))
2628
+ this.#reconcileSubmissions();
2411
2629
  }
2412
2630
  #submissionArgs(record) {
2413
2631
  const conversationId = record.conversationId;
@@ -2451,11 +2669,11 @@ class RemoteClient {
2451
2669
  record.phase = "inFlight";
2452
2670
  if (this.#entryPresent(record))
2453
2671
  this.#settleSubmission(record);
2672
+ } else if (this.#inboxHas(record)) {
2673
+ this.#settleSubmission(record);
2454
2674
  } else {
2455
2675
  record.phase = "inFlight";
2456
- if (record.conversationId !== undefined) {
2457
- record.checkedVersion = (this.#streamVersions.get(conversationStream(record.conversationId)) ?? 0) + (this.#streamVersions.get(docStream("pi.inbox", record.conversationId)) ?? 0);
2458
- }
2676
+ this.#markChecked(record);
2459
2677
  this.#confirmSubmission(record);
2460
2678
  }
2461
2679
  this.#touchSubmissions();
@@ -2701,7 +2919,7 @@ class RemoteClient {
2701
2919
  this.#reconcileSubmissions();
2702
2920
  const current = this.#current;
2703
2921
  const side = this.#side;
2704
- const conversation = current === undefined ? undefined : this.#values.get(conversationStream(current));
2922
+ const conversation = current === undefined ? undefined : this.#shownView(conversationStream(current));
2705
2923
  const summaries = this.#host("conversations") ?? [];
2706
2924
  const index = this.#host("index") ?? { projects: [], conversations: [] };
2707
2925
  const devices = this.#host("devices")?.devices ?? [];
@@ -2717,7 +2935,7 @@ class RemoteClient {
2717
2935
  submissions: this.#activeSubmissions(),
2718
2936
  side: side === undefined ? undefined : {
2719
2937
  id: side,
2720
- conversation: this.#values.get(conversationStream(side)),
2938
+ conversation: this.#shownView(conversationStream(side)),
2721
2939
  docs: this.#docsOf(side)
2722
2940
  },
2723
2941
  devices,
@@ -2726,6 +2944,7 @@ class RemoteClient {
2726
2944
  providers: this.#host("providers") ?? null,
2727
2945
  settings: this.#host("settings") ?? null,
2728
2946
  agents: this.#host("agents") ?? null,
2947
+ upgrade: this.#host("upgrade") ?? null,
2729
2948
  defaults: this.#effectiveDefaults(this.#helloDefaults)
2730
2949
  });
2731
2950
  }
@@ -2790,6 +3009,11 @@ class RemoteClient {
2790
3009
  }
2791
3010
  });
2792
3011
  }
3012
+ #requireUpgrade() {
3013
+ if (!hostSupportsCall(this.#hostMinor, "startUpgrade")) {
3014
+ throw new Error("The host is too old for this (needs protocol 5.30); run `pinomad upgrade` on the host");
3015
+ }
3016
+ }
2793
3017
  #command(operation) {
2794
3018
  this.#commands = this.#commands.then(operation).then(() => {}, (error) => this.#notice("error", error instanceof Error ? error.message : String(error)));
2795
3019
  return this.#commands;
@@ -2949,6 +3173,17 @@ class RemoteClient {
2949
3173
  });
2950
3174
  return outcome;
2951
3175
  }),
3176
+ reorderQueued: (submissionId, before, target) => this.#request(async () => {
3177
+ if (!hostSupportsCall(this.#hostMinor, "reorderQueued")) {
3178
+ throw new Error("The host is too old for this (needs protocol 5.29); run `pinomad upgrade` on the host");
3179
+ }
3180
+ const { outcome } = await this.#call("reorderQueued", {
3181
+ conversationId: target ?? conversationId(),
3182
+ submissionId,
3183
+ ...before === undefined ? {} : { before }
3184
+ });
3185
+ return outcome;
3186
+ }),
2952
3187
  setThinkingLevel: (level) => this.#command(() => this.#call("setThinkingLevel", { conversationId: conversationId(), level })),
2953
3188
  setModel: (model) => this.#command(() => this.#call("setModel", { conversationId: conversationId(), model })),
2954
3189
  setAgent: (agent) => this.#command(() => {
@@ -3017,6 +3252,7 @@ class RemoteClient {
3017
3252
  }
3018
3253
  return this.#call("listSkills", { scope });
3019
3254
  },
3255
+ loadEarlier: (id) => this.#loadEarlier(id),
3020
3256
  listCommands: (id) => {
3021
3257
  if (!hostSupportsCall(this.#hostMinor, "listCommands")) {
3022
3258
  return Promise.reject(new Error("The host is too old for this (needs protocol 5.17); run `pinomad upgrade` on the host"));
@@ -3065,8 +3301,23 @@ class RemoteClient {
3065
3301
  if (patch.hostName !== undefined && !hostSupportsFeature(this.#hostMinor, "hostName")) {
3066
3302
  throw new Error("The host is too old for this (needs protocol 5.12); run `pinomad upgrade` on the host");
3067
3303
  }
3304
+ if (patch.autoUpgrade !== undefined && !hostSupportsFeature(this.#hostMinor, "autoUpgrade")) {
3305
+ throw new Error("The host is too old for this (needs protocol 5.30); run `pinomad upgrade` on the host");
3306
+ }
3068
3307
  return this.#call("updateSettings", { patch });
3069
3308
  }),
3309
+ startUpgrade: async (when) => {
3310
+ this.#requireUpgrade();
3311
+ await this.#call("startUpgrade", { when });
3312
+ },
3313
+ cancelUpgrade: async () => {
3314
+ this.#requireUpgrade();
3315
+ await this.#call("cancelUpgrade", {});
3316
+ },
3317
+ checkUpgrade: async () => {
3318
+ this.#requireUpgrade();
3319
+ await this.#call("checkUpgrade", {});
3320
+ },
3070
3321
  createAgent: (role) => this.#request(async () => {
3071
3322
  if (!hostSupportsCall(this.#hostMinor, "createAgent")) {
3072
3323
  throw new Error("The host is too old for this (needs protocol 5.6); run `pinomad upgrade` on the host");
@@ -3103,7 +3354,7 @@ class RemoteClient {
3103
3354
  await this.#acquire(id);
3104
3355
  this.#current = id;
3105
3356
  if (previous !== undefined)
3106
- this.#release(previous, this.#side);
3357
+ this.#release(previous);
3107
3358
  this.#refresh();
3108
3359
  }
3109
3360
  #unshow() {
@@ -3111,7 +3362,7 @@ class RemoteClient {
3111
3362
  if (previous === undefined)
3112
3363
  return;
3113
3364
  this.#current = undefined;
3114
- this.#release(previous, this.#side);
3365
+ this.#release(previous);
3115
3366
  this.#refresh();
3116
3367
  }
3117
3368
  async#clear() {
@@ -3126,7 +3377,7 @@ class RemoteClient {
3126
3377
  await this.#acquire(id);
3127
3378
  this.#side = id;
3128
3379
  if (previous !== undefined)
3129
- this.#release(previous, this.#current);
3380
+ this.#release(previous);
3130
3381
  this.#refresh();
3131
3382
  }
3132
3383
  #close() {