@ccmsg/cli 0.11.0 → 0.11.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.11.0",
3
+ "version": "0.11.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",
@@ -188,9 +188,16 @@ const STARTING_PRESETS = [
188
188
  },
189
189
  {
190
190
  name: "journal",
191
- description: "日記用。人との往復と worker の答え、思考は要点だけ",
191
+ description: "日記用。人との往復と worker の答え、中断などの合図、思考は要点だけ",
192
192
  opts: {
193
- types: ["message.user", "message.parent", "message.sub.in", "message.team.in", "thinking"],
193
+ types: [
194
+ "message.user",
195
+ "message.parent",
196
+ "message.sub.in",
197
+ "message.team.in",
198
+ "notice",
199
+ "thinking",
200
+ ],
194
201
  },
195
202
  },
196
203
  {
@@ -1,6 +1,7 @@
1
1
  import { chmodSync, mkdirSync, unlinkSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { type Env, resolveSupervisorSocket } from "../instance/paths.ts";
4
+ import { WriteQueue } from "../transport/index.ts";
4
5
  import { CommandError, type SuperviseRequest } from "./link.ts";
5
6
  import {
6
7
  awaitGone,
@@ -194,11 +195,27 @@ export class Supervisor {
194
195
  }
195
196
  const handle = (frame: Record<string, unknown>): Promise<unknown> =>
196
197
  this.handle(frame as unknown as SuperviseRequest);
197
- this.#listener = Bun.listen<{ buffer: string }>({
198
+ this.#listener = Bun.listen<ControlState>({
198
199
  unix: path,
199
200
  socket: {
200
201
  open(socket) {
201
- socket.data = { buffer: "" };
202
+ // Every answer goes through the queue, because `socket.write` takes
203
+ // what fits in the socket buffer and returns a short count for the
204
+ // rest: an answer longer than that — `status --all` on a host with
205
+ // several instances — would otherwise arrive without its newline and
206
+ // leave the caller waiting for a line that never ends.
207
+ const queue = new WriteQueue<Uint8Array>({
208
+ encode: (line) => new TextEncoder().encode(line),
209
+ write(chunk) {
210
+ const written = socket.write(chunk);
211
+ if (written < 0) return undefined; // closing: nothing more will go
212
+ return written === chunk.length ? undefined : chunk.subarray(written);
213
+ },
214
+ flush: () => {
215
+ socket.flush();
216
+ },
217
+ });
218
+ socket.data = { buffer: "", queue };
202
219
  },
203
220
  data(socket, chunk) {
204
221
  socket.data.buffer += new TextDecoder().decode(chunk);
@@ -207,9 +224,12 @@ export class Supervisor {
207
224
  const line = socket.data.buffer.slice(0, at);
208
225
  socket.data.buffer = socket.data.buffer.slice(at + 1);
209
226
  if (line.trim() === "") continue;
210
- void answer(socket, line, handle);
227
+ void answer(socket.data.queue, line, handle);
211
228
  }
212
229
  },
230
+ drain(socket) {
231
+ socket.data.queue.drain();
232
+ },
213
233
  },
214
234
  });
215
235
  chmodSync(path, 0o600);
@@ -489,9 +509,16 @@ export class Supervisor {
489
509
  }
490
510
  }
491
511
 
512
+ /** What one control connection holds: the half-read line, and the answers
513
+ * waiting for a socket that is not taking them all at once. */
514
+ interface ControlState {
515
+ buffer: string;
516
+ queue: WriteQueue<Uint8Array>;
517
+ }
518
+
492
519
  /** Answer one line, in the shape a command reads: the result, or the error. */
493
520
  async function answer(
494
- socket: Bun.Socket<{ buffer: string }>,
521
+ queue: WriteQueue<Uint8Array>,
495
522
  line: string,
496
523
  handle: (frame: Record<string, unknown>) => Promise<unknown>,
497
524
  ): Promise<void> {
@@ -499,18 +526,18 @@ async function answer(
499
526
  try {
500
527
  frame = JSON.parse(line) as Record<string, unknown>;
501
528
  } catch {
502
- socket.write(
529
+ queue.push(
503
530
  `${JSON.stringify({ ok: false, error: { code: "bad_request", msg: "not valid JSON" } })}\n`,
504
531
  );
505
532
  return;
506
533
  }
507
534
  try {
508
- socket.write(`${JSON.stringify({ ok: true, result: await handle(frame) })}\n`);
535
+ queue.push(`${JSON.stringify({ ok: true, result: await handle(frame) })}\n`);
509
536
  } catch (cause) {
510
537
  const error =
511
538
  cause instanceof CommandError
512
539
  ? { code: cause.code, msg: cause.message }
513
540
  : { code: "internal_error", msg: String(cause) };
514
- socket.write(`${JSON.stringify({ ok: false, error })}\n`);
541
+ queue.push(`${JSON.stringify({ ok: false, error })}\n`);
515
542
  }
516
543
  }
@@ -83,31 +83,14 @@ function heading(file: SessionDumpFile, view: DumpView): string[] {
83
83
  function draw(item: Item, child: Item | undefined, view: DumpView, parent?: string): string[] {
84
84
  const own = fragment(item);
85
85
  const answer = child === undefined ? undefined : fragment(child);
86
- const nested = child !== undefined && spoken(child.type);
87
86
  const link = isResult(item)
88
87
  ? arrow("←", parent)
89
88
  : (arrow("→", fields(item)["result_item"]) ?? waiting(item));
90
89
  const head = isResult(item)
91
90
  ? words(prefix(item), link, own.head, clock(item))
92
- : words(
93
- prefix(item),
94
- own.head,
95
- link,
96
- nested || answer === undefined ? undefined : answer.head,
97
- clock(item),
98
- );
99
- const under = [
100
- ...body(own.body, view),
101
- ...(answer === undefined || nested ? [] : body(answer.body, view)),
102
- ];
103
- const lines = [head, ...under.map((line) => `${INDENT}${line}`)];
104
- if (!nested || child === undefined || answer === undefined) return lines;
105
- // The agent's answer, under the brief that asked for it. It keeps a heading
106
- // of its own — it has its own instant, and often a status the brief could
107
- // not have known — and is indented to say whose answer it is.
108
- lines.push(`${INDENT}${words(prefix(child), answer.head, clock(child))}`);
109
- for (const line of body(answer.body, view)) lines.push(`${INDENT}${INDENT}${line}`);
110
- return lines;
91
+ : words(prefix(item), own.head, link, answer?.head, clock(item));
92
+ const under = [...body(own.body, view), ...(answer === undefined ? [] : body(answer.body, view))];
93
+ return [head, ...under.map((line) => `${INDENT}${line}`)];
111
94
  }
112
95
 
113
96
  /** `[id] type`, which is how an item is pointed at: the id is what the links
@@ -238,9 +221,11 @@ function pair(items: readonly Item[]): {
238
221
  const call = where.get(to);
239
222
  if (call === undefined) continue;
240
223
  // A pair the reader would have to scroll between is left where each half
241
- // happened, unless it is an agent's: what an agent was asked and what it
242
- // answered are one exchange whatever fell between them.
243
- if (!spoken(item.type) && call !== at - 1) continue;
224
+ // happened. What is being read is a run of moments in the order they
225
+ // happened, and an answer that arrived minutes later — which is the usual
226
+ // case for an agent would put a later moment in the middle of an earlier
227
+ // one. The two halves point at each other by id instead.
228
+ if (call !== at - 1) continue;
244
229
  child.set(call, at);
245
230
  folded.add(at);
246
231
  }
@@ -255,12 +240,3 @@ function pair(items: readonly Item[]): {
255
240
  function joined(item: Item, key: string): string {
256
241
  return `${item.type.startsWith("message.") ? "message" : "tool"}\n${key}`;
257
242
  }
258
-
259
- /** Whether an item is the conversation half of starting an agent, as opposed
260
- * to the call's own half. What was asked and what came back is one exchange
261
- * however many turns fell between them, so these are drawn together wherever
262
- * they ended up — a conversation split across the page is one nobody can
263
- * follow. */
264
- function spoken(type: string): boolean {
265
- return type.startsWith("message.sub") || type.startsWith("message.team");
266
- }