@ccmsg/cli 0.6.0 → 0.6.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "The ccmsg daemon, CLI and agent plugins for one instance (= one config home)",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
@@ -20,7 +20,7 @@
20
20
  "test": "bun test"
21
21
  },
22
22
  "dependencies": {
23
- "@ccmsg/protocol": "1.15.0"
23
+ "@ccmsg/protocol": "1.16.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "^1.3.0",
@@ -227,6 +227,18 @@ export class Sessions implements UpstreamResource {
227
227
  ? undefined
228
228
  : new TerminalCache(deps.terminals, () => this.changed());
229
229
  this.#live = this.#liveNow(Date.now(), this.#own());
230
+ this.#reclaim(this.#live);
231
+ }
232
+
233
+ /** Drop the `last_live` entry of every session that is live, which is what
234
+ * keeps one session off both lists.
235
+ *
236
+ * Registering is one way a session comes back and is handled where it
237
+ * happens; the harness naming it again is the other, and it is the only one
238
+ * on a restart — nothing greets a daemon that was not there when the session
239
+ * started. */
240
+ #reclaim(live: ReadonlyMap<Sid, StoredEntry>): void {
241
+ for (const sid of live.keys()) this.#lastLive.remove(sid);
230
242
  }
231
243
 
232
244
  /** `hello`, which is where a session becomes something this instance can
@@ -483,9 +495,15 @@ export class Sessions implements UpstreamResource {
483
495
  return this.#harness.running;
484
496
  }
485
497
 
486
- /** The `peers` payload: what is connected now, and what was connected when
487
- * this instance last saw it. Both travel together because registering is
488
- * exactly what moves a session from the second list to the first.
498
+ /** The `peers` payload: what is live now, and what was live when this
499
+ * instance last saw it. Both travel together because coming back is exactly
500
+ * what moves a session from the second list to the first.
501
+ *
502
+ * Live is not the same as connected (§5.2). A session the harness names is
503
+ * live whether or not it ever greeted us, and it has to be on this list for
504
+ * the same reason it is classified at all: a restart forgets every greeting,
505
+ * and a list that showed only what had greeted this daemon would show a host
506
+ * full of running sessions as empty.
489
507
  *
490
508
  * Every row states its `state` and its `pinned`. The contract lets an
491
509
  * instance leave them out, and a client then shows a session it cannot group
@@ -504,7 +522,12 @@ export class Sessions implements UpstreamResource {
504
522
  ): { peers: PeerInfo[]; last_live: LastLiveSession[]; instances?: InstanceInfo[] } {
505
523
  const instances = this.deps.mesh?.instances();
506
524
  return {
507
- peers: [...this.#connected.values()].map((session) => this.#peer(session, now, own)),
525
+ peers: [
526
+ ...[...this.#connected.values()].map((session) => this.#peer(session, now, own)),
527
+ ...[...own.present]
528
+ .filter((sid) => !this.#connected.has(sid))
529
+ .map((sid) => this.#unconnected(sid, now, own)),
530
+ ],
508
531
  last_live: this.#lastLive.entries(now).map((entry) => ({
509
532
  ...entry,
510
533
  state: this.classify(entry.sid, now, own) ?? "disappeared",
@@ -597,6 +620,7 @@ export class Sessions implements UpstreamResource {
597
620
  // comes back says them again.
598
621
  this.#stated.delete(sid);
599
622
  }
623
+ this.#reclaim(live);
600
624
  this.#live = live;
601
625
  this.deps.publish("peers", this.peers(now, own));
602
626
  this.deps.publish("agents", this.agents(own));
@@ -661,6 +685,36 @@ export class Sessions implements UpstreamResource {
661
685
  };
662
686
  }
663
687
 
688
+ /** A session the harness names that holds no connection here (§5.1).
689
+ *
690
+ * It is on the same list as the connected ones because it is live in the same
691
+ * sense: the classification is what separates them, and a client groups on
692
+ * that field alone (§5.2). What it cannot carry is everything a greeting
693
+ * states — the session never said where it works, so the working directory
694
+ * comes from the harness's own row and the display names it does not know are
695
+ * simply absent.
696
+ *
697
+ * The connection fields go with the connection: `connected_at`,
698
+ * `last_activity_at` and the client's build and generation are things about a
699
+ * client of this session, and there is none. */
700
+ #unconnected(sid: Sid, now: Timestamp, own: Own): PeerInfo {
701
+ const row = own.rows.get(sid);
702
+ const userInput = this.deps.transcript?.facts(sid).last_user_input_at;
703
+ const gatewayActiveAt = this.#gatewayActiveAt(sid, true);
704
+ return {
705
+ sid,
706
+ instance: this.deps.self,
707
+ // The harness knows a title for a session that stated none itself, and
708
+ // what the session said about itself overrides it.
709
+ ...(row?.name === undefined ? {} : { title: row.name }),
710
+ ...this.#where(sid, own),
711
+ state: this.classify(sid, now, own) ?? "live",
712
+ pinned: this.#pinned(sid),
713
+ ...(userInput === undefined ? {} : { last_user_input_at: userInput }),
714
+ ...(gatewayActiveAt === undefined ? {} : { gateway_active_at: gatewayActiveAt }),
715
+ };
716
+ }
717
+
664
718
  /** When the gateway last saw inference for a session, for a session this
665
719
  * instance knows (§5.1).
666
720
  *