@magnusekdahl/parallix 1.0.3 → 1.0.4

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/README.md CHANGED
@@ -40,7 +40,7 @@ Each capability below is tied to a use case in [`docs/use-cases.md`](docs/use-ca
40
40
  - **Publish work to a Forgejo reviewer surface without making Forgejo your branch authority** *(Confirmed mechanic).* When the review provider is enabled, Parallix syncs the local baseline to a dedicated `review` remote and opens or updates the PR there; if Forgejo is disabled, the branch/worktree flow still runs locally.
41
41
  - **Use a repo-local Graphify knowledge graph for smaller codebase context pulls** *(Confirmed mechanic, optional, unproven payoff).* In repositories where the operator has already installed the Graphify skill, the workflow keeps `graphify-out/` isolated per worktree and refreshes it during review/integration, while the installed agent guidance steers codebase questions toward `graphify query` / `path` / `explain` before full reports or raw grep. That should reduce context bloat, but this repo does not currently claim a measured token-usage reduction.
42
42
  - **Keep your existing verification gate instead of agent self-reporting** *(UC-5 — Confirmed).* The gate is a configured shell command with a no-op default: declare your existing `make` / `npm` / script command in `workflow.config.json` and it runs verbatim; declare nothing and verification is a documented no-op pass, not an invented gate.
43
- - **See which agent family actually pays off across every repo one runtime drives** *(UC-6 — Partial).* A single operator-owned `stats.csv` accumulates per-agent telemetry across repositories. Token-cost comparison is complete today only for the families with structured telemetry (codex, claude); two families record honest zeros by design.
43
+ - **See which agent family actually pays off across every repo one runtime drives** *(UC-6 — Partial).* A single operator-owned `stats.csv` accumulates per-agent usage telemetry across repositories. Token-cost comparison is complete today only for the families with structured telemetry (codex, claude, opencode/qwen); vibe/mistral record honest zeros by design.
44
44
 
45
45
  ## The core workflow
46
46
 
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const childProcess = require('child_process');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
4
7
 
5
8
  /**
6
9
  * Capture the complete `opencode export <sessionId>` JSON document.
@@ -15,6 +18,10 @@ const childProcess = require('child_process');
15
18
  * - Fails explicitly (resolves null) when output exceeds `maxBytes` rather
16
19
  * than silently truncating into unparseable JSON.
17
20
  *
21
+ * Uses a temp-file fd for stdout to avoid pipe-buffer data loss that occurs
22
+ * with large (>100 KB) child-process output. The child writes directly to
23
+ * the fd; after exit we read the file back as a string.
24
+ *
18
25
  * Never rejects: any error/timeout/oversize resolves to null so telemetry
19
26
  * capture stays best-effort and cannot break the launch.
20
27
  *
@@ -44,13 +51,21 @@ function captureOpencodeExport(sessionId, opts = {}) {
44
51
 
45
52
  let settled = false;
46
53
  let timer = null;
47
- const chunks = [];
48
54
  let size = 0;
55
+ let tmpFd = null;
56
+ let tmpPath = null;
49
57
 
50
58
  const finish = (value) => {
51
59
  if (settled) return;
52
60
  settled = true;
53
61
  if (timer) clearTimeout(timer);
62
+ // Clean up temp file
63
+ if (tmpFd !== null) {
64
+ try { fs.closeSync(tmpFd); } catch (_) { /* already closed */ }
65
+ }
66
+ if (tmpPath) {
67
+ try { fs.unlinkSync(tmpPath); } catch (_) { /* already gone */ }
68
+ }
54
69
  resolve(value);
55
70
  };
56
71
 
@@ -58,18 +73,33 @@ function captureOpencodeExport(sessionId, opts = {}) {
58
73
  try { child.kill('SIGKILL'); } catch (_) { /* already gone */ }
59
74
  };
60
75
 
76
+ // Create a temp file for stdout to avoid pipe-buffer truncation
77
+ let tmpFileError = null;
78
+ try {
79
+ tmpPath = path.join(os.tmpdir(), `opencode-export-${process.pid}-${Date.now()}.json`);
80
+ tmpFd = fs.openSync(tmpPath, 'w');
81
+ } catch (e) {
82
+ tmpFileError = e;
83
+ }
84
+
61
85
  let child;
62
86
  try {
63
87
  child = spawn('opencode', ['export', sessionId], {
64
88
  cwd: worktree,
65
89
  env: { ...process.env, ...(env || {}) },
66
- stdio: ['ignore', 'pipe', 'ignore'],
90
+ stdio: ['ignore', tmpFd !== null ? tmpFd : 'pipe', 'pipe'],
67
91
  });
68
92
  } catch (_) {
69
93
  finish(null);
70
94
  return;
71
95
  }
72
96
 
97
+ if (tmpFileError) {
98
+ killChild(child);
99
+ finish(null);
100
+ return;
101
+ }
102
+
73
103
  timer = setTimeout(() => {
74
104
  // Hung or slow export: kill it and degrade to null. Do not block.
75
105
  killChild(child);
@@ -80,29 +110,42 @@ function captureOpencodeExport(sessionId, opts = {}) {
80
110
  // ref'd keeps the timeout deterministic under Node's test runner (an unref'd
81
111
  // timer can be skipped when the loop is otherwise idle, cancelling the test).
82
112
 
83
- if (!child || !child.stdout) {
84
- finish(null);
85
- return;
113
+ // Consume stderr to prevent pipe-buffer issues with the opencode binary
114
+ if (child.stderr) {
115
+ child.stderr.on('data', () => { /* drain stderr */ });
86
116
  }
87
117
 
88
- child.stdout.on('data', (chunk) => {
118
+ child.on('error', () => finish(null));
119
+
120
+ child.on('close', (code) => {
89
121
  if (settled) return;
90
- size += chunk.length;
122
+ if (tmpFd === null) {
123
+ finish(null);
124
+ return;
125
+ }
126
+
127
+ try {
128
+ fs.closeSync(tmpFd);
129
+ } catch (_) { /* already closed */ }
130
+ tmpFd = null;
131
+
132
+ let content;
133
+ try {
134
+ content = fs.readFileSync(tmpPath, 'utf8');
135
+ } catch (_) {
136
+ finish(null);
137
+ return;
138
+ }
139
+
140
+ size = Buffer.byteLength(content);
91
141
  if (size > maxBytes) {
92
142
  // Explicit failure rather than silent truncation: a partial JSON
93
143
  // document would parse to null/garbage and fabricate zero telemetry.
94
- killChild(child);
95
144
  finish(null);
96
145
  return;
97
146
  }
98
- chunks.push(chunk);
99
- });
100
147
 
101
- child.on('error', () => finish(null));
102
-
103
- child.on('close', () => {
104
- if (settled) return;
105
- finish(Buffer.concat(chunks).toString('utf8'));
148
+ finish(content);
106
149
  });
107
150
  });
108
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magnusekdahl/parallix",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "AI mission workflow toolkit with a px CLI — local-first, human-in-the-loop multi-agent development",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "private": false,