@ccmsg/cli 0.8.0 → 0.8.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.8.0",
3
+ "version": "0.8.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",
@@ -421,6 +421,9 @@ export class Instance {
421
421
  onActivity: () => {
422
422
  this.#sessions.refresh();
423
423
  },
424
+ onMoved: (sid) => {
425
+ this.#sessions.gatewayMoved(sid);
426
+ },
424
427
  log: (msg, fields) => {
425
428
  this.log.write(msg, fields);
426
429
  },
@@ -536,14 +536,52 @@ export class Sessions implements UpstreamResource {
536
536
  ...[...own.present]
537
537
  .filter((sid) => !this.#connected.has(sid))
538
538
  .map((sid) => this.#unconnected(sid, now, own)),
539
- ...this.#lastLive.entries(now).map((entry) => ({
540
- ...entry,
541
- state: this.classify(entry.sid, now, own) ?? "disappeared",
542
- pinned: this.#pinned(entry.sid),
543
- })),
539
+ ...this.#lastLive.entries(now).map((entry) => this.#lost(entry, now, own)),
544
540
  ];
545
541
  }
546
542
 
543
+ /** One row of `peers`, for a producer that knows which session moved.
544
+ *
545
+ * The same three sources the list is built from, asked about one sid: a
546
+ * connection here, a session the harness names, an entry among the sessions
547
+ * this instance has lost. A sid none of them holds is one this instance has
548
+ * no row for, and it says so rather than inventing one. */
549
+ #peerRow(sid: Sid, now: Timestamp, own: Own): PeerInfo | undefined {
550
+ const held = this.#connected.get(sid);
551
+ if (held !== undefined) return this.#peer(held, now, own);
552
+ if (own.present.has(sid)) return this.#unconnected(sid, now, own);
553
+ const entry = this.#lastLive.get(sid);
554
+ return entry === undefined ? undefined : this.#lost(entry, now, own);
555
+ }
556
+
557
+ /** A session this instance has lost, as a row: what was observed of it,
558
+ * with the two fields a row derives worked out at read time (M4). */
559
+ #lost(entry: StoredEntry, now: Timestamp, own: Own): PeerInfo {
560
+ return {
561
+ ...entry,
562
+ state: this.classify(entry.sid, now, own) ?? "disappeared",
563
+ pinned: this.#pinned(entry.sid),
564
+ };
565
+ }
566
+
567
+ /** The gateway saw inference for one session again (§5.1).
568
+ *
569
+ * What moved is one attribute of one row, so that row is what goes out. The
570
+ * sessions domain is not recomputed for it: which sessions there are has not
571
+ * changed, and the whole of that work would be spent to restate a clock.
572
+ *
573
+ * A sid this instance has no row for publishes nothing. The gateway sits
574
+ * above every config home and its events name only a session id, so one
575
+ * belonging to another config home must not become a row here — the same
576
+ * narrowing the row's own reading of the gateway makes. */
577
+ gatewayMoved(sid: Sid): void {
578
+ const now = Date.now();
579
+ const row = this.#peerRow(sid, now, this.#own());
580
+ if (row === undefined) return;
581
+ const peers = this.#sentPeers.diffRow(row) as PeerElement[];
582
+ if (peers.length > 0) this.deps.publish("peers", { peers });
583
+ }
584
+
547
585
  /** Every row `agents` states: the harness's own view, as it stated it. */
548
586
  agentRows(own: Own = this.#own()): AgentInfo[] {
549
587
  return [...own.rows.values()];
Binary file
@@ -114,6 +114,9 @@ export interface GatewayDeps {
114
114
  /** The gateway saw something happen for a session, which is an input of the
115
115
  * sessions domain (§5.1) rather than of either topic. */
116
116
  readonly onActivity?: () => void;
117
+ /** A session already known to be running was seen again: its clock moved,
118
+ * and the row that carries it is what says so. */
119
+ readonly onMoved?: (sid: Sid) => void;
117
120
  readonly log?: (msg: string, fields?: Record<string, unknown>) => void;
118
121
  /** Replaces the outward read in tests. */
119
122
  readonly fetch?: typeof fetch;
@@ -139,6 +142,7 @@ export class Gateway {
139
142
  self: deps.self,
140
143
  publish: deps.publish,
141
144
  ...(deps.onActivity === undefined ? {} : { onActivity: deps.onActivity }),
145
+ ...(deps.onMoved === undefined ? {} : { onMoved: deps.onMoved }),
142
146
  });
143
147
  this.status =
144
148
  deps.setup.statusUrl === undefined
@@ -14,16 +14,23 @@ export interface LlmRequestsDeps {
14
14
  /** The one way a value reaches subscribers (§6.1). */
15
15
  readonly publish: (topic: string, data: unknown) => void;
16
16
  /** An event moved when a session was last seen running inference, which is
17
- * an input of the sessions domain (§5.1) and not of this topic. */
17
+ * an input of the sessions domain (§5.1) and not of this topic. Told when
18
+ * the window opened, which is the moment the classification can change. */
18
19
  readonly onActivity?: () => void;
20
+ /** The same session seen again inside a window already open: one attribute
21
+ * of one row moved. Told apart from the above because what it asks for is
22
+ * that row restated rather than the whole domain recomputed. */
23
+ readonly onMoved?: (sid: Sid) => void;
19
24
  }
20
25
 
21
26
  /** Which of a session's gateway facts moved.
22
27
  *
23
- * `live` is the only one the sessions domain has to hear: it is the moment the
24
- * classification of §5.1 can change, because the window either opened or
25
- * closed. `clock` is the same session seen again inside a window that was
26
- * already open — the value of an attribute, not a section anything is in. */
28
+ * `live` is the moment the classification of §5.1 can change, because the
29
+ * window either opened or closed, and the sessions domain recomputes for it.
30
+ * `clock` is the same session seen again inside a window that was already open
31
+ * — the value of an attribute, not a section anything is in — so what it asks
32
+ * for is that one row restated. Both reach a subscriber; they differ in how
33
+ * much work is done to say so. */
27
34
  type GatewayMove = "live" | "clock" | "none";
28
35
 
29
36
  /** Series remembered at once. The prune below already holds this near the
@@ -77,7 +84,7 @@ export class LlmRequests implements UpstreamResource {
77
84
  * near-ordered in practice, but a redelivery can put an older one after a
78
85
  * newer, and a countdown must not walk backwards. */
79
86
  record(info: LlmRequestObservation): void {
80
- if (this.active(info.sid, info.received_at) === "live") this.deps.onActivity?.();
87
+ this.moved(info.sid, this.active(info.sid, info.received_at));
81
88
  const key = seriesKey(info.sid, info.prefix);
82
89
  const held = this.#series.get(key);
83
90
  if (held !== undefined && held.info.received_at >= info.received_at) return;
@@ -99,7 +106,13 @@ export class LlmRequests implements UpstreamResource {
99
106
  * the window belongs to the request that opened it — and only says the
100
107
  * session was still running inference at that instant. */
101
108
  note(sid: Sid, at: Timestamp): void {
102
- if (this.active(sid, at) === "live") this.deps.onActivity?.();
109
+ this.moved(sid, this.active(sid, at));
110
+ }
111
+
112
+ /** Tell whoever holds the row what this event moved for that session. */
113
+ private moved(sid: Sid, move: GatewayMove): void {
114
+ if (move === "live") this.deps.onActivity?.();
115
+ else if (move === "clock") this.deps.onMoved?.(sid);
103
116
  }
104
117
 
105
118
  /** When the gateway last saw inference for a session (§5.1). Undefined once
@@ -148,11 +161,10 @@ export class LlmRequests implements UpstreamResource {
148
161
 
149
162
  /** Note the session was seen, and say what that moved.
150
163
  *
151
- * A session already inside its window moves its clock and nothing else. The
152
- * sessions domain is told about `live` alone, because telling it about every
153
- * event would restate the whole of `peers` once per call a session makes:
154
- * inference is observed several times a second and the row it lands on
155
- * differs only in an attribute (§5.2). */
164
+ * A session already inside its window moves its clock and nothing else, so
165
+ * the row it lands on is restated on its own: inference is observed several
166
+ * times a second, and recomputing the domain for each would spend the whole
167
+ * of that work on one attribute of one row (§5.2). */
156
168
  private active(sid: Sid, at: Timestamp): GatewayMove {
157
169
  const held = this.#activeAt.get(sid);
158
170
  if (held !== undefined && held >= at) return "none";