@ccmsg/cli 0.14.0 → 0.14.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.14.0",
3
+ "version": "0.14.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",
@@ -564,7 +564,9 @@ export class Instance {
564
564
  },
565
565
  });
566
566
 
567
- const inbox = new Inbox(inboxPath(paths.stateDir));
567
+ const inbox = new Inbox(inboxPath(paths.stateDir), (msg, fields) => {
568
+ this.log.write(msg, fields);
569
+ });
568
570
  inbox.load();
569
571
  this.#persisted.push(inbox);
570
572
  // Route (a) is the harness's own way in (DESIGN §6.5): Claude Code's messaging
@@ -4,10 +4,12 @@ import { dirname, join } from "node:path";
4
4
  import {
5
5
  INBOX_MAX_PER_SID,
6
6
  INBOX_RETENTION_MS,
7
- type InboxMessage,
7
+ InboxMessage,
8
8
  type InboxRemovedReason,
9
- type Sid,
9
+ isValid,
10
+ Sid,
10
11
  type Timestamp,
12
+ validationErrors,
11
13
  } from "@ccmsg/protocol";
12
14
 
13
15
  export const INBOX_FILE = "inbox.jsonl";
@@ -50,7 +52,12 @@ export class Inbox {
50
52
  * subscribed for them to be news to. */
51
53
  #onRemoved?: (mid: string, reason: InboxRemovedReason) => void;
52
54
 
53
- constructor(private readonly file: string) {}
55
+ constructor(
56
+ private readonly file: string,
57
+ /** Where a line the replay could not keep is named. Absent in the tests
58
+ * that are about the holding rather than about what is said of it. */
59
+ private readonly log: (message: string, fields: Record<string, unknown>) => void = () => {},
60
+ ) {}
54
61
 
55
62
  /** Hear about messages leaving. */
56
63
  onRemoved(told: (mid: string, reason: InboxRemovedReason) => void): void {
@@ -79,6 +86,7 @@ export class Inbox {
79
86
  // The last line of a file the daemon was killed while writing.
80
87
  continue;
81
88
  }
89
+ if (record.v === "add" && !this.#stateable(record.sid, record.message)) continue;
82
90
  this.#replay(record);
83
91
  }
84
92
  this.#expire(now);
@@ -168,6 +176,30 @@ export class Inbox {
168
176
  return highest;
169
177
  }
170
178
 
179
+ /** Whether a line read back is a message the contract can state.
180
+ *
181
+ * The file outlives the contract that wrote it, and what is held is answered
182
+ * to a person as rows of the `inbox` topic — so a single line the contract
183
+ * has since outgrown, replayed as if it were current, is a frame the reader
184
+ * refuses and a whole view lost for it. An instance states only what the
185
+ * contract can say, about its own file as much as about anything else.
186
+ *
187
+ * Dropped rather than mended: the message is the sender's words and the
188
+ * contract is what says how they are spelled, so there is nothing here that
189
+ * could write a spelling the contract would accept without inventing it. It
190
+ * leaves through the compaction that follows the replay, which writes back
191
+ * only what is held; nothing is appended and no removal is stated, since a
192
+ * replay has nobody subscribed to hear one and no `mid` the contract would
193
+ * take to name it by. */
194
+ #stateable(sid: unknown, message: unknown): boolean {
195
+ const why = isValid(Sid, sid)
196
+ ? validationErrors(InboxMessage, message)
197
+ : [`sid: ${JSON.stringify(sid)} is no session id`];
198
+ if (why.length === 0) return true;
199
+ this.log("dropped an inbox record the contract cannot state", { sid, why });
200
+ return false;
201
+ }
202
+
171
203
  #replay(record: Record_): void {
172
204
  if (record.v === "add") {
173
205
  const held = this.#held.get(record.sid) ?? [];