@synergenius/flow-weaver 0.26.4 → 0.26.6
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.
|
@@ -60,14 +60,31 @@ export declare class CliSession {
|
|
|
60
60
|
* to prevent TOCTOU races.
|
|
61
61
|
*/
|
|
62
62
|
send(userMessage: string, systemPromptPrefix?: string): AsyncGenerator<StreamEvent>;
|
|
63
|
+
/** Whether a send() call is currently active. */
|
|
64
|
+
get hasActiveTurn(): boolean;
|
|
63
65
|
/**
|
|
64
66
|
* Kill the CLI process.
|
|
65
67
|
*/
|
|
66
68
|
kill(): void;
|
|
69
|
+
/** Callback for session kill events — wire to audit logging. */
|
|
70
|
+
private _onSessionKilled?;
|
|
71
|
+
/** Register a callback that fires when this session is killed. */
|
|
72
|
+
set onSessionKilled(cb: ((info: {
|
|
73
|
+
sessionId: string;
|
|
74
|
+
hadActiveTurn: boolean;
|
|
75
|
+
eventsInTurn: number;
|
|
76
|
+
}) => void) | undefined);
|
|
67
77
|
/**
|
|
68
78
|
* Compute a fingerprint of CLI-relevant options for cache comparison.
|
|
69
79
|
* Field order is significant for JSON.stringify comparison.
|
|
70
80
|
* Add new CLI-relevant fields here when they're added to CliSessionOptions.
|
|
81
|
+
*
|
|
82
|
+
* NOTE: mcpConfigPath is EXCLUDED. It's an ephemeral temp path that changes
|
|
83
|
+
* on every createMcpBridge() call. Including it causes fingerprint mismatches
|
|
84
|
+
* that kill active sessions mid-turn when a second provider is created for
|
|
85
|
+
* the same project (e.g., orchestrator + worker sharing the same projectDir).
|
|
86
|
+
* The MCP bridge is swapped per-request via setHandlers() — the config path
|
|
87
|
+
* is only needed at spawn time, not for session identity.
|
|
71
88
|
*/
|
|
72
89
|
private static fingerprint;
|
|
73
90
|
private pushEvent;
|
|
@@ -81,6 +98,13 @@ export declare class CliSession {
|
|
|
81
98
|
* Get an existing session or create a new one.
|
|
82
99
|
* Phase 1.4: Validates that cached sessions have matching CLI-relevant options.
|
|
83
100
|
* If options changed on the same key, the old session is killed and recreated.
|
|
101
|
+
*
|
|
102
|
+
* SAFETY: Refuses to kill a session with an active turn. This prevents data
|
|
103
|
+
* loss when multiple providers share the same session key (e.g., orchestrator
|
|
104
|
+
* and worker both using projectDir as key). If the fingerprint doesn't match
|
|
105
|
+
* but the session has an active turn, we return the existing session and log
|
|
106
|
+
* a warning — better to reuse a session with slightly different options than
|
|
107
|
+
* to kill an active conversation and lose costUsd/result data.
|
|
84
108
|
*/
|
|
85
109
|
export declare function getOrCreateCliSession(key: string, options: CliSessionOptions): CliSession;
|
|
86
110
|
/**
|
|
@@ -20,9 +20,20 @@
|
|
|
20
20
|
* 4. The CLI uses NDJSON for MCP stdio transport (not Content-Length framing).
|
|
21
21
|
*/
|
|
22
22
|
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
24
|
+
import { resolve, dirname } from 'node:path';
|
|
23
25
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
24
27
|
import { StreamJsonParser } from './streaming.js';
|
|
25
28
|
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
29
|
+
// Package version included in session fingerprint so cached sessions
|
|
30
|
+
// auto-invalidate when the core is updated (e.g. npm update).
|
|
31
|
+
let CORE_VERSION = 'unknown';
|
|
32
|
+
try {
|
|
33
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
34
|
+
CORE_VERSION = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
|
|
35
|
+
}
|
|
36
|
+
catch { /* non-fatal */ }
|
|
26
37
|
export class CliSession {
|
|
27
38
|
sessionId = randomUUID();
|
|
28
39
|
child = null;
|
|
@@ -246,14 +257,31 @@ export class CliSession {
|
|
|
246
257
|
}
|
|
247
258
|
}
|
|
248
259
|
finally {
|
|
260
|
+
// Log how the turn ended for debugging cost/result event issues.
|
|
261
|
+
// If turn.done is false here, the generator was abandoned by the consumer
|
|
262
|
+
// (e.g., for-await broke out) before completeTurn/markDead fired.
|
|
263
|
+
// This means the result event hasn't been processed yet, and any
|
|
264
|
+
// subsequent stdout data (including the result) will be silently dropped
|
|
265
|
+
// because activeTurn is about to be set to null.
|
|
266
|
+
if (!turn.done) {
|
|
267
|
+
process.stderr.write(`\x1b[33m ⚠ CliSession: generator abandoned before turn.done (events=${turn.events.length}). Result event will be lost.\x1b[0m\n`);
|
|
268
|
+
}
|
|
249
269
|
this.activeTurn = null;
|
|
250
270
|
this.resetIdleTimer();
|
|
251
271
|
}
|
|
252
272
|
}
|
|
273
|
+
/** Whether a send() call is currently active. */
|
|
274
|
+
get hasActiveTurn() {
|
|
275
|
+
return this.activeTurn !== null && !this.activeTurn.done;
|
|
276
|
+
}
|
|
253
277
|
/**
|
|
254
278
|
* Kill the CLI process.
|
|
255
279
|
*/
|
|
256
280
|
kill() {
|
|
281
|
+
if (this.hasActiveTurn) {
|
|
282
|
+
process.stderr.write(`\x1b[31m ✗ CliSession.kill() called with active turn — data loss will occur (session=${this.sessionId.slice(0, 8)})\x1b[0m\n`);
|
|
283
|
+
this._onSessionKilled?.({ sessionId: this.sessionId, hadActiveTurn: true, eventsInTurn: this.activeTurn.events.length });
|
|
284
|
+
}
|
|
257
285
|
this.clearIdleTimer();
|
|
258
286
|
if (this.child && this.alive) {
|
|
259
287
|
this.log?.info('Killing CLI session', { sessionId: this.sessionId });
|
|
@@ -268,6 +296,12 @@ export class CliSession {
|
|
|
268
296
|
}
|
|
269
297
|
this.markDead();
|
|
270
298
|
}
|
|
299
|
+
/** Callback for session kill events — wire to audit logging. */
|
|
300
|
+
_onSessionKilled;
|
|
301
|
+
/** Register a callback that fires when this session is killed. */
|
|
302
|
+
set onSessionKilled(cb) {
|
|
303
|
+
this._onSessionKilled = cb;
|
|
304
|
+
}
|
|
271
305
|
// ---------------------------------------------------------------------------
|
|
272
306
|
// Private
|
|
273
307
|
// ---------------------------------------------------------------------------
|
|
@@ -275,11 +309,19 @@ export class CliSession {
|
|
|
275
309
|
* Compute a fingerprint of CLI-relevant options for cache comparison.
|
|
276
310
|
* Field order is significant for JSON.stringify comparison.
|
|
277
311
|
* Add new CLI-relevant fields here when they're added to CliSessionOptions.
|
|
312
|
+
*
|
|
313
|
+
* NOTE: mcpConfigPath is EXCLUDED. It's an ephemeral temp path that changes
|
|
314
|
+
* on every createMcpBridge() call. Including it causes fingerprint mismatches
|
|
315
|
+
* that kill active sessions mid-turn when a second provider is created for
|
|
316
|
+
* the same project (e.g., orchestrator + worker sharing the same projectDir).
|
|
317
|
+
* The MCP bridge is swapped per-request via setHandlers() — the config path
|
|
318
|
+
* is only needed at spawn time, not for session identity.
|
|
278
319
|
*/
|
|
279
320
|
static fingerprint(options) {
|
|
280
321
|
return JSON.stringify({
|
|
322
|
+
_coreVersion: CORE_VERSION, // auto-invalidate cache on core update
|
|
281
323
|
model: options.model,
|
|
282
|
-
mcpConfigPath
|
|
324
|
+
// mcpConfigPath deliberately excluded — see comment above
|
|
283
325
|
strictMcpConfig: options.strictMcpConfig,
|
|
284
326
|
disallowedTools: options.disallowedTools,
|
|
285
327
|
tools: options.tools,
|
|
@@ -306,6 +348,26 @@ export class CliSession {
|
|
|
306
348
|
this.cleanupFn?.();
|
|
307
349
|
this.cleanupFn = null;
|
|
308
350
|
if (this.activeTurn && !this.activeTurn.done) {
|
|
351
|
+
// KNOWN ISSUE: On long orchestrator runs (many MCP tool calls), the CLI
|
|
352
|
+
// process can die before emitting the `result` event. This means:
|
|
353
|
+
// - costUsd from total_cost_usd is lost (stays 0)
|
|
354
|
+
// - cacheReadTokens / cacheCreationTokens are lost
|
|
355
|
+
// - The turn ends via markDead instead of completeTurn
|
|
356
|
+
// When this happens, consumers should fall back to cost-update events
|
|
357
|
+
// from the global usage callback for accurate cost tracking.
|
|
358
|
+
// See: project_costUsd_bench_issue.md in memory for full investigation.
|
|
359
|
+
const eventCount = this.activeTurn.events.length;
|
|
360
|
+
const hasResult = this.activeTurn.events.some((e) => e.type === 'usage' && e.costUsd != null);
|
|
361
|
+
if (!hasResult) {
|
|
362
|
+
const lastStderr = this.stderrBuf.slice(-500);
|
|
363
|
+
this.log?.warn('CLI session died before result event — costUsd will be 0', {
|
|
364
|
+
sessionId: this.sessionId,
|
|
365
|
+
eventsInTurn: eventCount,
|
|
366
|
+
lastStderr: lastStderr || '(empty)',
|
|
367
|
+
});
|
|
368
|
+
// Always log to stderr so it's visible in bench output
|
|
369
|
+
process.stderr.write(`\x1b[33m ⚠ CLI session died before result event (${eventCount} events buffered). costUsd will be 0. stderr: ${lastStderr.slice(0, 200)}\x1b[0m\n`);
|
|
370
|
+
}
|
|
309
371
|
if (!this.activeTurn.events.some((e) => e.type === 'message_stop')) {
|
|
310
372
|
this.activeTurn.events.push({ type: 'message_stop', finishReason: 'error' });
|
|
311
373
|
}
|
|
@@ -345,6 +407,13 @@ const sessions = new Map();
|
|
|
345
407
|
* Get an existing session or create a new one.
|
|
346
408
|
* Phase 1.4: Validates that cached sessions have matching CLI-relevant options.
|
|
347
409
|
* If options changed on the same key, the old session is killed and recreated.
|
|
410
|
+
*
|
|
411
|
+
* SAFETY: Refuses to kill a session with an active turn. This prevents data
|
|
412
|
+
* loss when multiple providers share the same session key (e.g., orchestrator
|
|
413
|
+
* and worker both using projectDir as key). If the fingerprint doesn't match
|
|
414
|
+
* but the session has an active turn, we return the existing session and log
|
|
415
|
+
* a warning — better to reuse a session with slightly different options than
|
|
416
|
+
* to kill an active conversation and lose costUsd/result data.
|
|
348
417
|
*/
|
|
349
418
|
export function getOrCreateCliSession(key, options) {
|
|
350
419
|
const existing = sessions.get(key);
|
|
@@ -353,7 +422,13 @@ export function getOrCreateCliSession(key, options) {
|
|
|
353
422
|
if (existing.matchesOptions(options)) {
|
|
354
423
|
return existing;
|
|
355
424
|
}
|
|
356
|
-
// Options changed — kill
|
|
425
|
+
// Options changed — but REFUSE to kill if there's an active turn
|
|
426
|
+
if (existing.hasActiveTurn) {
|
|
427
|
+
process.stderr.write(`\x1b[33m ⚠ getOrCreateCliSession: fingerprint mismatch on key "${key}" but session has active turn — reusing existing session to prevent data loss\x1b[0m\n`);
|
|
428
|
+
return existing;
|
|
429
|
+
}
|
|
430
|
+
// No active turn — safe to kill and recreate
|
|
431
|
+
process.stderr.write(`\x1b[2m [session-cache] killing session for key "${key}" — fingerprint changed\x1b[0m\n`);
|
|
357
432
|
existing.kill();
|
|
358
433
|
sessions.delete(key);
|
|
359
434
|
}
|
package/dist/cli/flow-weaver.mjs
CHANGED
|
@@ -9886,7 +9886,7 @@ var VERSION;
|
|
|
9886
9886
|
var init_generated_version = __esm({
|
|
9887
9887
|
"src/generated-version.ts"() {
|
|
9888
9888
|
"use strict";
|
|
9889
|
-
VERSION = "0.26.
|
|
9889
|
+
VERSION = "0.26.6";
|
|
9890
9890
|
}
|
|
9891
9891
|
});
|
|
9892
9892
|
|
|
@@ -95973,7 +95973,7 @@ function parseIntStrict(value) {
|
|
|
95973
95973
|
// src/cli/index.ts
|
|
95974
95974
|
init_logger();
|
|
95975
95975
|
init_error_utils();
|
|
95976
|
-
var version2 = true ? "0.26.
|
|
95976
|
+
var version2 = true ? "0.26.6" : "0.0.0-dev";
|
|
95977
95977
|
var program2 = new Command();
|
|
95978
95978
|
program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
|
|
95979
95979
|
logger.banner(version2);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.26.
|
|
1
|
+
export declare const VERSION = "0.26.6";
|
|
2
2
|
//# sourceMappingURL=generated-version.d.ts.map
|
package/package.json
CHANGED