c8ctl-plugin-nano 1.44.6 → 1.44.7

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 (2) hide show
  1. package/c8ctl-plugin.js +87 -0
  2. package/package.json +8 -8
package/c8ctl-plugin.js CHANGED
@@ -4568,6 +4568,15 @@ function spawnCaptureAcp({ command, args = [], cwd, env, stdinData, timeoutMs, i
4568
4568
  // One-time warning latch for the reserved escalate/filter policies so the
4569
4569
  // deferral is observable (not silent) but never spams a warning per request.
4570
4570
  let interimWarned = false;
4571
+ // #137: count of canonical transcript chunks actually PUBLISHED to the relay's
4572
+ // transcript-chunk seam, plus a one-time latch for the fallback floor. Some ACP
4573
+ // agents (e.g. copilot 1.0.82 on some hosts) complete a turn without ever
4574
+ // emitting a mappable `session/update`, so `emitTranscript` never fires and the
4575
+ // cockpit drill-in would show an empty session. When the turn completes having
4576
+ // published zero chunks, we synthesise ONE structured floor chunk so the
4577
+ // drill-in shows the outcome instead of nothing (see `maybeEmitTranscriptFloor`).
4578
+ let transcriptChunksPublished = 0;
4579
+ let floorEmitted = false;
4571
4580
 
4572
4581
  // Live "spy" tee (--stream), line-buffered, mirroring the other paths.
4573
4582
  // Separate line buffers per lane (stdout-human vs stderr) so a partial line
@@ -4638,6 +4647,13 @@ function spawnCaptureAcp({ command, args = [], cwd, env, stdinData, timeoutMs, i
4638
4647
  if (idleMon) idleMon.stop();
4639
4648
  if (settleTimer) { clearTimeout(settleTimer); settleTimer = null; }
4640
4649
  if (detachSteer) { try { detachSteer(); } catch { /* best effort */ } detachSteer = null; }
4650
+ // #137: a turn that completed but produced no mappable session/update gets a
4651
+ // synthesised structured floor so the cockpit drill-in isn't empty. Only for
4652
+ // a resolved turn (`promptResolved`) — a handshake/spawn failure has nothing
4653
+ // to floor and its error is surfaced in the result envelope. Best-effort and
4654
+ // guarded, so it can never turn a settle into a throw. Runs before the tee is
4655
+ // flushed so a --stream watcher sees the floored line too.
4656
+ if (promptResolved) { try { maybeEmitTranscriptFloor(); } catch { /* floor best-effort */ } }
4641
4657
  if (teeSink) { tee('', true); teeErr('', true); }
4642
4658
  // Reap the child if it is still alive (turn resolved but agent lingering).
4643
4659
  try { if (child && childClosed === null) killTree(child); } catch { /* best effort */ }
@@ -4775,6 +4791,7 @@ function spawnCaptureAcp({ command, args = [], cwd, env, stdinData, timeoutMs, i
4775
4791
  let published = false;
4776
4792
  try { relayTap.relayTranscriptChunk(chunk); published = true; } catch { /* relay best-effort */ }
4777
4793
  if (published) {
4794
+ transcriptChunksPublished++;
4778
4795
  // Mirror the human text locally (spy tee + captured stdout) so the
4779
4796
  // result envelope and --stream spy are unchanged — without re-emitting
4780
4797
  // raw text onto the relay lane, which now carries the canonical chunk.
@@ -4787,6 +4804,76 @@ function spawnCaptureAcp({ command, args = [], cwd, env, stdinData, timeoutMs, i
4787
4804
  emitHuman(describeUpdate(update));
4788
4805
  };
4789
4806
 
4807
+ // #137: build the human-readable text for a fallback transcript FLOOR — used
4808
+ // only when a turn completed having produced no mappable session/update, so
4809
+ // the cockpit drill-in shows the run's OUTCOME instead of an empty session.
4810
+ // Prefer the agent's own structured result (from $AGENT_RESULT_FILE: summary,
4811
+ // status, pr), then its stderr diagnostics, then a fixed explanatory note.
4812
+ const buildFloorText = () => {
4813
+ const resultPath = env && typeof env === 'object' ? env[AGENT_RESULT_FILE_ENV] : null;
4814
+ const res = readAgentResultFile(resultPath);
4815
+ if (res && typeof res === 'object') {
4816
+ const parts = [];
4817
+ if (typeof res.summary === 'string' && res.summary.trim()) parts.push(res.summary.trim());
4818
+ if (typeof res.status === 'string' && res.status.trim()) parts.push(`(status: ${res.status.trim()})`);
4819
+ if (typeof res.pr === 'string' && res.pr.trim()) parts.push(`PR: ${res.pr.trim()}`);
4820
+ else if (res.pr && typeof res.pr === 'object' && typeof res.pr.url === 'string' && res.pr.url.trim()) parts.push(`PR: ${res.pr.url.trim()}`);
4821
+ if (parts.length) return parts.join(' ');
4822
+ }
4823
+ const diag = joinCapped(stderrChunks).trim();
4824
+ if (diag) {
4825
+ // Surface the tail of the agent's stderr (its own diagnostics) as the
4826
+ // floor — capped so a chatty agent can't blow up a single transcript card.
4827
+ const FLOOR_DIAG_CAP = 2_000;
4828
+ const tail = diag.length > FLOOR_DIAG_CAP ? `…${diag.slice(diag.length - FLOOR_DIAG_CAP)}` : diag;
4829
+ return `Agent published no structured/canonical transcript content. Diagnostics:\n${tail}`;
4830
+ }
4831
+ return 'Agent completed the turn but published no structured/canonical transcript content, so no transcript messages were produced.';
4832
+ };
4833
+
4834
+ // #137: synthesise ONE structured transcript-chunk FLOOR when a completed turn
4835
+ // published zero canonical chunks. The floor rides the SAME canonical bridge
4836
+ // (an `agent_message_chunk` run through `acpUpdateToTranscriptChunk`) so the
4837
+ // cockpit renders a real assistant-message card — never a hand-rolled envelope
4838
+ // — and honours the acceptance's "structured/raw transcript floor" so the
4839
+ // drill-in shows something useful rather than an empty session. Idempotent
4840
+ // (one-time latch), inert unless the relay exposes the transcript-chunk seam,
4841
+ // and skipped entirely when any real chunk already reached the lane.
4842
+ const maybeEmitTranscriptFloor = () => {
4843
+ if (floorEmitted) return;
4844
+ if (transcriptChunksPublished > 0) return;
4845
+ if (!relayTap || typeof relayTap.relayTranscriptChunk !== 'function') return;
4846
+ floorEmitted = true;
4847
+ const text = buildFloorText();
4848
+ if (!text) return;
4849
+ const chunk = encodeTranscriptChunk({ sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } });
4850
+ if (!chunk) {
4851
+ // Canonical bridge couldn't produce a chunk (bridge throw or unmapped
4852
+ // classification) → emit the floor on the text lane rather than dropping
4853
+ // it, exactly like `emitTranscript`, so the cockpit transcript is never
4854
+ // left empty in the very failure mode this floor exists to mitigate.
4855
+ emitHuman(text);
4856
+ return;
4857
+ }
4858
+ // Only skip the text lane when the chunk publish ACTUALLY succeeded. If the
4859
+ // seam throws (a downstream tap implementation bug, not just the built-in
4860
+ // best-effort guard), the floor never reached the relay lane — so fall back
4861
+ // to the text lane, exactly like `emitTranscript`, or the cockpit transcript
4862
+ // could still be empty in that failure mode.
4863
+ let published = false;
4864
+ try { relayTap.relayTranscriptChunk(chunk); published = true; } catch { /* relay best-effort */ }
4865
+ if (published) {
4866
+ transcriptChunksPublished++;
4867
+ // Mirror locally (spy tee + captured stdout) so a --stream watcher and the
4868
+ // result envelope also reflect the floor, matching the mapped-update path.
4869
+ captureHuman(text);
4870
+ return;
4871
+ }
4872
+ // Chunk publish threw → emit the floor on the text lane (relay text + spy
4873
+ // tee + capture) so nothing is dropped.
4874
+ emitHuman(text);
4875
+ };
4876
+
4790
4877
  const handleMessage = (msg) => {
4791
4878
  if (!msg || typeof msg !== 'object') return;
4792
4879
  // A response to one of OUR requests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.44.6",
3
+ "version": "1.44.7",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -57,12 +57,12 @@
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "node-pty": "^1.0.0",
60
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.6",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.6",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.6",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.6",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.6",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.6",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.6"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.7",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.7",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.7",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.7",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.7",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.7",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.7"
67
67
  }
68
68
  }