@tom2012/cc-web 2026.5.13-a → 2026.5.14-a
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/backend/dist/flows/runner.d.ts +4 -11
- package/backend/dist/flows/runner.d.ts.map +1 -1
- package/backend/dist/flows/runner.js +190 -381
- package/backend/dist/flows/runner.js.map +1 -1
- package/backend/dist/flows/store.d.ts +22 -18
- package/backend/dist/flows/store.d.ts.map +1 -1
- package/backend/dist/flows/store.js +68 -60
- package/backend/dist/flows/store.js.map +1 -1
- package/backend/dist/flows/types.d.ts +103 -75
- package/backend/dist/flows/types.d.ts.map +1 -1
- package/backend/dist/flows/types.js +20 -8
- package/backend/dist/flows/types.js.map +1 -1
- package/backend/dist/routes/flows.d.ts.map +1 -1
- package/backend/dist/routes/flows.js +184 -146
- package/backend/dist/routes/flows.js.map +1 -1
- package/frontend/dist/assets/{ChatOverlay-DeV9Kxv1.js → ChatOverlay-DsfZXjjz.js} +1 -1
- package/frontend/dist/assets/{GraphPreview-DKBe95dN.js → GraphPreview-BGZHA8KW.js} +1 -1
- package/frontend/dist/assets/{MobilePage-l30eYbBv.js → MobilePage-B-FiVfUE.js} +3 -3
- package/frontend/dist/assets/{OfficePreview-CZZ5h3xg.js → OfficePreview-CvHPeAlI.js} +2 -2
- package/frontend/dist/assets/{PdfPreview-Db1BlGXF.js → PdfPreview-D1OVG7jG.js} +1 -1
- package/frontend/dist/assets/{ProjectPage-BSuP0OrL.js → ProjectPage-CyKEenc5.js} +5 -5
- package/frontend/dist/assets/{SettingsPage-B9iJQRJi.js → SettingsPage-CIe3HZml.js} +1 -1
- package/frontend/dist/assets/{SkillHubPage-CDdKniVk.js → SkillHubPage-CG1-G4Pf.js} +1 -1
- package/frontend/dist/assets/{chevron-down-CO9XV3WE.js → chevron-down-CS7uu2Ol.js} +1 -1
- package/frontend/dist/assets/{index-CHWSKceq.js → index-BThL9NV3.js} +2 -2
- package/frontend/dist/assets/index-BkQ6KI1l.css +1 -0
- package/frontend/dist/assets/{index-sRc5noTy.js → index-DCc5jmus.js} +1 -1
- package/frontend/dist/assets/{index-CwqaFIAN.js → index-DN91i4kg.js} +1 -1
- package/frontend/dist/assets/{jszip.min-DQDZjRrf.js → jszip.min-DHcltCpp.js} +1 -1
- package/frontend/dist/assets/{select-D1f26o6o.js → select-C09dwvOR.js} +1 -1
- package/frontend/dist/assets/{user-CCZuBByN.js → user-Bj8X-WCQ.js} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-RmHssrlH.css +0 -1
|
@@ -40,135 +40,115 @@ const events_1 = require("events");
|
|
|
40
40
|
const uuid_1 = require("uuid");
|
|
41
41
|
const logger_1 = require("../logger");
|
|
42
42
|
const store_1 = require("./store");
|
|
43
|
-
const types_1 = require("./types");
|
|
44
43
|
const log = (0, logger_1.modLogger)('flow-runner');
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
/** Wrap text in a bracketed-paste sequence + trailing CR.
|
|
45
|
+
* Strips all ESC sequences and CRs from the body — escape sequences embedded
|
|
46
|
+
* in either promptTemplate (codex P2-G) or variable values would otherwise
|
|
47
|
+
* corrupt Ink TUI state or close paste mode prematurely. LF and TAB are
|
|
48
|
+
* preserved so the body's structure (paragraphs, code indentation) is
|
|
49
|
+
* intact. The final CR is re-added after the close-marker to submit. */
|
|
48
50
|
function buildPaste(text) {
|
|
49
|
-
const safe = text.replace(
|
|
51
|
+
const safe = text.replace(/[\x1b\r]/g, '');
|
|
50
52
|
return `\x1b[200~${safe}\x1b[201~\r`;
|
|
51
53
|
}
|
|
52
|
-
|
|
53
|
-
* Missing files render as `[ERROR reading <path>: <reason>]` — the runner
|
|
54
|
-
* separately surfaces read failures via provider-aware error routing
|
|
55
|
-
* before we get here, so this substitution path is only a defense for
|
|
56
|
-
* unexpected misses. */
|
|
57
|
-
function renderTemplate(folderPath, tpl) {
|
|
58
|
-
return tpl.replace(/\{\{file:([^}]+)\}\}/g, (_m, rel) => {
|
|
59
|
-
const abs = (0, store_1.safeProjectPath)(folderPath, rel.trim());
|
|
60
|
-
if (!abs)
|
|
61
|
-
return `[ERROR unsafe path rejected: ${rel}]`;
|
|
62
|
-
try {
|
|
63
|
-
return fs.readFileSync(abs, 'utf-8');
|
|
64
|
-
}
|
|
65
|
-
catch (err) {
|
|
66
|
-
return `[ERROR reading ${rel}: ${err instanceof Error ? err.message : 'unknown'}]`;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
54
|
+
// ── Value rendering & sanitization ────────────────────────────────────────
|
|
70
55
|
/** Strip terminal control bytes that would corrupt bracketed-paste mode or
|
|
71
56
|
* Ink TUI state when the value is later injected into a prompt. ESC sequences
|
|
72
57
|
* (incl. paste-mode markers) can confuse the agent's input parser; bare CR
|
|
73
58
|
* prematurely closes paste mode. We keep LF and TAB so multi-line content
|
|
74
59
|
* renders normally. */
|
|
75
|
-
function
|
|
60
|
+
function sanitizeForPrompt(s) {
|
|
76
61
|
return s.replace(/[\x1b\r]/g, '');
|
|
77
62
|
}
|
|
78
|
-
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
return '';
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
|
90
|
-
return '';
|
|
91
|
-
const val = obj[v.name];
|
|
92
|
-
if (val === undefined || val === null)
|
|
93
|
-
return '';
|
|
94
|
-
const str = typeof val === 'string' ? val : JSON.stringify(val);
|
|
95
|
-
return sanitizeVarValue(str);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return '';
|
|
99
|
-
}
|
|
63
|
+
/** Format an arbitrary JSON value for prompt injection. Strings pass through
|
|
64
|
+
* (after sanitize); other values get JSON.stringify with 2-space indent so
|
|
65
|
+
* arrays/objects are human-readable to the LLM. `undefined` becomes the
|
|
66
|
+
* literal "(未设置)" marker so the LLM knows the variable hasn't been
|
|
67
|
+
* written yet rather than seeing a missing field silently. */
|
|
68
|
+
function formatValueForPrompt(value) {
|
|
69
|
+
if (value === undefined || value === null)
|
|
70
|
+
return '(未设置)';
|
|
71
|
+
if (typeof value === 'string')
|
|
72
|
+
return sanitizeForPrompt(value);
|
|
73
|
+
return sanitizeForPrompt(JSON.stringify(value, null, 2));
|
|
100
74
|
}
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
catch {
|
|
117
|
-
/* missing or unparseable → start from {} */
|
|
118
|
-
}
|
|
119
|
-
Object.assign(existing, kv);
|
|
120
|
-
try {
|
|
121
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
122
|
-
fs.writeFileSync(abs, JSON.stringify(existing, null, 2));
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
return { ok: false, error: `failed to write ${file}: ${err instanceof Error ? err.message : 'unknown'}` };
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return { ok: true };
|
|
75
|
+
/** Substitute `{{var:name}}` and `{{const:name}}` tokens in the prompt
|
|
76
|
+
* template with the current value from workflow_data. Unset declared
|
|
77
|
+
* variables (those without initialValue and not yet written) render as
|
|
78
|
+
* "(未设置)" — the validator has already gated on the name being declared
|
|
79
|
+
* at save time, so an unknown name at runtime is impossible by construction.
|
|
80
|
+
* (codex P1-C: previously this returned "[ERROR ...]" which leaked into
|
|
81
|
+
* prompts whenever a declared-but-uninitialized variable was referenced.) */
|
|
82
|
+
function renderTemplate(tpl, data) {
|
|
83
|
+
return tpl
|
|
84
|
+
.replace(/\{\{var:([^}]+)\}\}/g, (_m, name) => {
|
|
85
|
+
return formatValueForPrompt(data.variables[name.trim()]);
|
|
86
|
+
})
|
|
87
|
+
.replace(/\{\{const:([^}]+)\}\}/g, (_m, name) => {
|
|
88
|
+
return formatValueForPrompt(data.constants[name.trim()]);
|
|
89
|
+
});
|
|
129
90
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (refNames.length === 0)
|
|
91
|
+
// ── Prompt block builders ─────────────────────────────────────────────────
|
|
92
|
+
/** Build a "current variable values" context block for the prompt head. */
|
|
93
|
+
function buildReadVarBlock(names, def, data) {
|
|
94
|
+
if (names.length === 0)
|
|
135
95
|
return '';
|
|
136
|
-
const byName = new Map(variables.map((v) => [v.name, v]));
|
|
137
|
-
const lines = [];
|
|
138
|
-
|
|
139
|
-
for (const name of refNames) {
|
|
96
|
+
const byName = new Map((def.variables ?? []).map((v) => [v.name, v]));
|
|
97
|
+
const lines = ['──────── 流变量当前值 ────────'];
|
|
98
|
+
for (const name of names) {
|
|
140
99
|
const v = byName.get(name);
|
|
141
100
|
if (!v)
|
|
142
101
|
continue;
|
|
143
|
-
const value = readVariableValue(folderPath, v);
|
|
144
102
|
const meaning = v.description || '(无描述)';
|
|
145
103
|
lines.push(`变量 \`${v.name}\`(含义:${meaning}):`);
|
|
146
|
-
lines.push(
|
|
104
|
+
lines.push(formatValueForPrompt(data.variables[name]));
|
|
147
105
|
lines.push('');
|
|
148
106
|
}
|
|
149
107
|
lines.push('');
|
|
150
108
|
return lines.join('\n');
|
|
151
109
|
}
|
|
152
|
-
/** Build
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (initNames.length === 0)
|
|
110
|
+
/** Build a "constants" context block. Constants are stable per run, so we
|
|
111
|
+
* just dump value + description. */
|
|
112
|
+
function buildReadConstBlock(names, def, data) {
|
|
113
|
+
if (names.length === 0)
|
|
157
114
|
return '';
|
|
158
|
-
const byName = new Map(
|
|
159
|
-
const lines = [];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
115
|
+
const byName = new Map((def.constants ?? []).map((c) => [c.name, c]));
|
|
116
|
+
const lines = ['──────── 流常量 ────────'];
|
|
117
|
+
for (const name of names) {
|
|
118
|
+
const c = byName.get(name);
|
|
119
|
+
if (!c)
|
|
120
|
+
continue;
|
|
121
|
+
const meaning = c.description || '(无描述)';
|
|
122
|
+
lines.push(`常量 \`${c.name}\`(含义:${meaning}):`);
|
|
123
|
+
lines.push(formatValueForPrompt(data.constants[name]));
|
|
124
|
+
lines.push('');
|
|
125
|
+
}
|
|
126
|
+
lines.push('');
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
/** Build the prompt suffix instructing the LLM to write each `writeVariables`
|
|
130
|
+
* entry into workflow_data.variables[name]. */
|
|
131
|
+
function buildWriteVarBlock(names, def) {
|
|
132
|
+
if (names.length === 0)
|
|
133
|
+
return '';
|
|
134
|
+
const byName = new Map((def.variables ?? []).map((v) => [v.name, v]));
|
|
135
|
+
const lines = [
|
|
136
|
+
'',
|
|
137
|
+
'──────── 变量写入指令 ────────',
|
|
138
|
+
'完成本任务后,请按下面列出的含义判断每个变量的值,',
|
|
139
|
+
'用 Edit 或 Write 工具把它们写入 .ccweb/workflow_data.json 的 variables 字段(顶层 key = 变量名)。',
|
|
140
|
+
'保留 workflow_data.json 中其他变量、常量、task_progress 字段不要动。',
|
|
141
|
+
'',
|
|
142
|
+
];
|
|
143
|
+
for (const name of names) {
|
|
165
144
|
const v = byName.get(name);
|
|
166
145
|
if (!v)
|
|
167
146
|
continue;
|
|
168
|
-
lines.push(`- \`${v.name}\` →
|
|
147
|
+
lines.push(`- \`${v.name}\` → 写入 variables.${v.name}。含义:${v.description || '(无描述)'}`);
|
|
169
148
|
}
|
|
170
149
|
return lines.join('\n');
|
|
171
150
|
}
|
|
151
|
+
// ── Branch evaluation ─────────────────────────────────────────────────────
|
|
172
152
|
/** Loose equality for branch evaluation. JSON outputs from LLMs frequently
|
|
173
153
|
* type-shift (`true` → `"true"`, `1` → `"1"`); branch authors typically
|
|
174
154
|
* configure the typed primitive, so we coerce common cases. */
|
|
@@ -193,34 +173,6 @@ function branchMatches(value, expected) {
|
|
|
193
173
|
}
|
|
194
174
|
return false;
|
|
195
175
|
}
|
|
196
|
-
function readInputs(folderPath, inputs) {
|
|
197
|
-
for (const inp of inputs) {
|
|
198
|
-
const abs = (0, store_1.safeProjectPath)(folderPath, inp.path);
|
|
199
|
-
if (!abs) {
|
|
200
|
-
return {
|
|
201
|
-
ok: false,
|
|
202
|
-
failingProvider: inp.provider,
|
|
203
|
-
error: `unsafe path rejected: ${inp.path}`,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
const raw = fs.readFileSync(abs, 'utf-8');
|
|
208
|
-
// Best-effort JSON parse — non-JSON inputs (e.g. bibtex) pass through
|
|
209
|
-
// here as long as the file exists; a stricter parse, if needed, lives
|
|
210
|
-
// in the consuming node (e.g. system-logic parses JSON itself).
|
|
211
|
-
if (inp.path.endsWith('.json'))
|
|
212
|
-
JSON.parse(raw);
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
return {
|
|
216
|
-
ok: false,
|
|
217
|
-
failingProvider: inp.provider,
|
|
218
|
-
error: err instanceof Error ? err.message : String(err),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return { ok: true };
|
|
223
|
-
}
|
|
224
176
|
class FlowRunner extends events_1.EventEmitter {
|
|
225
177
|
constructor() {
|
|
226
178
|
super(...arguments);
|
|
@@ -230,7 +182,6 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
230
182
|
setPromptInjector(fn) {
|
|
231
183
|
this.injector = fn;
|
|
232
184
|
}
|
|
233
|
-
/** Returns null if a flow is already running for this project. */
|
|
234
185
|
start(projectId, folderPath, flowDef, flowFilename) {
|
|
235
186
|
if (this.active.has(projectId)) {
|
|
236
187
|
return { ok: false, reason: 'already-running' };
|
|
@@ -238,7 +189,9 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
238
189
|
const startNode = flowDef.nodes.find((n) => n.id === flowDef.entryNodeId);
|
|
239
190
|
if (!startNode)
|
|
240
191
|
return { ok: false, reason: 'entry-node-not-found' };
|
|
241
|
-
|
|
192
|
+
// Initialize workflow_data: constants written once, variables get
|
|
193
|
+
// initialValue (where declared), task_progress reset for this run.
|
|
194
|
+
(0, store_1.initWorkflowData)(folderPath, flowDef);
|
|
242
195
|
const state = {
|
|
243
196
|
flowId: flowDef.id,
|
|
244
197
|
flowFilename,
|
|
@@ -263,7 +216,6 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
263
216
|
userInputResolve: null,
|
|
264
217
|
userInputReject: null,
|
|
265
218
|
currentTaskIndex: null,
|
|
266
|
-
pendingLlmError: null,
|
|
267
219
|
};
|
|
268
220
|
this.active.set(projectId, run);
|
|
269
221
|
this.emit('state', { projectId, state });
|
|
@@ -271,27 +223,19 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
271
223
|
projectId,
|
|
272
224
|
flowId: flowDef.id,
|
|
273
225
|
flowName: flowDef.name,
|
|
226
|
+
schemaVersion: flowDef.schemaVersion,
|
|
274
227
|
entryNodeId: flowDef.entryNodeId,
|
|
275
228
|
nodeCount: flowDef.nodes.length,
|
|
229
|
+
constantCount: (flowDef.constants ?? []).length,
|
|
230
|
+
variableCount: (flowDef.variables ?? []).length,
|
|
276
231
|
runId: state.runId,
|
|
277
232
|
}, 'flow start');
|
|
278
|
-
// Fire-and-forget; the loop persists state on each transition.
|
|
279
233
|
void this.runLoop(run).catch((err) => {
|
|
280
234
|
log.error({ projectId, err: err instanceof Error ? err.message : String(err) }, 'run loop crashed');
|
|
281
235
|
this.finalize(run, 'failed', err instanceof Error ? err.message : String(err));
|
|
282
236
|
});
|
|
283
237
|
return { ok: true, state };
|
|
284
238
|
}
|
|
285
|
-
/**
|
|
286
|
-
* Resume a paused run by re-executing the current node. Caller intent:
|
|
287
|
-
* - timeout → re-inject prompt, wait again
|
|
288
|
-
* - max-retries-exceeded → reset that node's loop counter so user gets
|
|
289
|
-
* a fresh allotment (otherwise resume would immediately hit the same
|
|
290
|
-
* cap and pause again)
|
|
291
|
-
* - file-read-error → user fixed the file; re-read succeeds and node
|
|
292
|
-
* proceeds normally
|
|
293
|
-
* - awaiting-user-input → wrong path; caller should use submitUserInput
|
|
294
|
-
*/
|
|
295
239
|
resume(projectId) {
|
|
296
240
|
const run = this.active.get(projectId);
|
|
297
241
|
if (!run)
|
|
@@ -330,9 +274,6 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
330
274
|
const hadTaskWait = !!wait;
|
|
331
275
|
run.userInputResolve = null;
|
|
332
276
|
run.userInputReject = null;
|
|
333
|
-
// settle() inside waitForTaskFinish calls clearWaiters internally, so
|
|
334
|
-
// we don't need to call it here for the wait path. For the user-input
|
|
335
|
-
// path we still need finalize to clean up (no watcher/timer there).
|
|
336
277
|
wait?.('aborted');
|
|
337
278
|
userReject?.('aborted');
|
|
338
279
|
log.info({ projectId, currentNodeId: run.state.currentNodeId, hadTaskWait, hadUserInputWait }, 'flow abort');
|
|
@@ -347,8 +288,6 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
347
288
|
const resolve = run.userInputResolve;
|
|
348
289
|
run.userInputResolve = null;
|
|
349
290
|
run.userInputReject = null;
|
|
350
|
-
// Log keys + value lengths only — field values may contain user research
|
|
351
|
-
// goals / unpublished hypotheses that we don't want in plaintext logs.
|
|
352
291
|
log.info({
|
|
353
292
|
projectId,
|
|
354
293
|
currentNodeId: run.state.currentNodeId,
|
|
@@ -390,7 +329,6 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
390
329
|
: outcome.kind === 'retry' ? 'retry'
|
|
391
330
|
: 'error';
|
|
392
331
|
if (outcome.kind === 'pause' || outcome.kind === 'error') {
|
|
393
|
-
// executeNode already set status/pauseReason; persist + bail out.
|
|
394
332
|
this.persist(run);
|
|
395
333
|
return;
|
|
396
334
|
}
|
|
@@ -402,7 +340,6 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
402
340
|
return;
|
|
403
341
|
}
|
|
404
342
|
}
|
|
405
|
-
// 'retry' just persists; loop continues with same state.currentNodeId
|
|
406
343
|
}
|
|
407
344
|
}
|
|
408
345
|
// ── Node executors ────────────────────────────────────────────────────
|
|
@@ -418,24 +355,31 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
418
355
|
return { kind: 'error', message: `unknown node kind: ${node.kind}` };
|
|
419
356
|
}
|
|
420
357
|
async executeUserInput(run, node) {
|
|
421
|
-
//
|
|
422
|
-
// them read-only without
|
|
423
|
-
const
|
|
424
|
-
const
|
|
358
|
+
// Snapshot context values (bindVariable / bindConstant) so the frontend
|
|
359
|
+
// can render them read-only without a separate fetch.
|
|
360
|
+
const data = (0, store_1.readWorkflowData)(run.folderPath);
|
|
361
|
+
const variablesCtx = {};
|
|
362
|
+
const constantsCtx = {};
|
|
425
363
|
for (const field of node.userInputSchema.fields) {
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
364
|
+
if (field.bindVariable && field.bindVariable in data.variables) {
|
|
365
|
+
variablesCtx[field.bindVariable] = data.variables[field.bindVariable];
|
|
366
|
+
}
|
|
367
|
+
if (field.bindConstant && field.bindConstant in data.constants) {
|
|
368
|
+
constantsCtx[field.bindConstant] = data.constants[field.bindConstant];
|
|
369
|
+
}
|
|
432
370
|
}
|
|
371
|
+
const contextValues = Object.keys(variablesCtx).length > 0 || Object.keys(constantsCtx).length > 0
|
|
372
|
+
? {
|
|
373
|
+
variables: Object.keys(variablesCtx).length > 0 ? variablesCtx : undefined,
|
|
374
|
+
constants: Object.keys(constantsCtx).length > 0 ? constantsCtx : undefined,
|
|
375
|
+
}
|
|
376
|
+
: undefined;
|
|
433
377
|
run.state.status = 'paused';
|
|
434
378
|
run.state.pauseReason = 'awaiting-user-input';
|
|
435
379
|
run.state.pendingUserInput = {
|
|
436
380
|
nodeId: node.id,
|
|
437
381
|
fields: node.userInputSchema.fields,
|
|
438
|
-
|
|
382
|
+
contextValues,
|
|
439
383
|
};
|
|
440
384
|
this.persist(run);
|
|
441
385
|
this.emit('user-input', { projectId: run.projectId, nodeId: node.id, fields: node.userInputSchema.fields });
|
|
@@ -443,78 +387,33 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
443
387
|
projectId: run.projectId,
|
|
444
388
|
nodeId: node.id,
|
|
445
389
|
fieldKeys: node.userInputSchema.fields.map((f) => f.key),
|
|
446
|
-
outputCount: node.outputs.length,
|
|
447
390
|
}, 'flow node user-input awaiting');
|
|
448
|
-
let
|
|
391
|
+
let submitted;
|
|
449
392
|
try {
|
|
450
|
-
|
|
393
|
+
submitted = await new Promise((resolve, reject) => {
|
|
451
394
|
run.userInputResolve = resolve;
|
|
452
395
|
run.userInputReject = reject;
|
|
453
396
|
});
|
|
454
397
|
}
|
|
455
|
-
catch
|
|
456
|
-
// Reject via abort() — outer handler does finalize.
|
|
398
|
+
catch {
|
|
457
399
|
return { kind: 'pause' };
|
|
458
400
|
}
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
// the
|
|
462
|
-
//
|
|
401
|
+
// Merge field values into variables. For bindVariable / bindConstant
|
|
402
|
+
// fields we re-read from workflow_data (defense — client could lie even
|
|
403
|
+
// though the UI disables those inputs); for outputVariable fields we
|
|
404
|
+
// take the submitted value as-is.
|
|
405
|
+
const fresh = (0, store_1.readWorkflowData)(run.folderPath);
|
|
406
|
+
const variableUpdates = {};
|
|
463
407
|
for (const field of node.userInputSchema.fields) {
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
const v = variables.find((x) => x.name === field.bindVariable);
|
|
467
|
-
if (!v)
|
|
468
|
-
continue;
|
|
469
|
-
data[field.key] = readVariableValue(run.folderPath, v);
|
|
470
|
-
}
|
|
471
|
-
// Write outputs: synthesize a JSON object from user fields and write to
|
|
472
|
-
// each declared output file. For Phase 1, multi-output user-input nodes
|
|
473
|
-
// get the same payload written to each — keeps the schema simple.
|
|
474
|
-
const payload = {};
|
|
475
|
-
for (const field of node.userInputSchema.fields)
|
|
476
|
-
payload[field.key] = data[field.key] ?? '';
|
|
477
|
-
// Merge values for fields with outputToVariable into the named variable's
|
|
478
|
-
// file (read-modify-write — multiple variables may share one file).
|
|
479
|
-
const variableUpdates = new Map();
|
|
480
|
-
for (const field of node.userInputSchema.fields) {
|
|
481
|
-
if (!field.outputToVariable)
|
|
482
|
-
continue;
|
|
483
|
-
const v = variables.find((x) => x.name === field.outputToVariable);
|
|
484
|
-
if (!v)
|
|
485
|
-
continue;
|
|
486
|
-
const file = v.file || types_1.DEFAULT_VAR_FILE;
|
|
487
|
-
if (!variableUpdates.has(file))
|
|
488
|
-
variableUpdates.set(file, {});
|
|
489
|
-
variableUpdates.get(file)[v.name] = data[field.key] ?? '';
|
|
490
|
-
}
|
|
491
|
-
if (variableUpdates.size > 0) {
|
|
492
|
-
const wr = writeVariableValues(run.folderPath, variableUpdates);
|
|
493
|
-
if (!wr.ok) {
|
|
494
|
-
run.state.status = 'failed';
|
|
495
|
-
run.state.pauseReason = null;
|
|
496
|
-
run.state.pauseDetail = wr.error ?? 'failed to write variables';
|
|
497
|
-
return { kind: 'error', message: run.state.pauseDetail };
|
|
408
|
+
if (field.outputVariable) {
|
|
409
|
+
variableUpdates[field.outputVariable] = submitted[field.key] ?? '';
|
|
498
410
|
}
|
|
411
|
+
// bindVariable / bindConstant fields contribute nothing to writes —
|
|
412
|
+
// they're read-only displays.
|
|
499
413
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
run.state.status = 'failed';
|
|
504
|
-
run.state.pauseReason = null;
|
|
505
|
-
run.state.pauseDetail = `unsafe output path rejected: ${out.path}`;
|
|
506
|
-
return { kind: 'error', message: run.state.pauseDetail };
|
|
507
|
-
}
|
|
508
|
-
try {
|
|
509
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
510
|
-
fs.writeFileSync(abs, JSON.stringify(payload, null, 2));
|
|
511
|
-
}
|
|
512
|
-
catch (err) {
|
|
513
|
-
run.state.status = 'failed';
|
|
514
|
-
run.state.pauseReason = null;
|
|
515
|
-
run.state.pauseDetail = `failed to write ${out.path}: ${err instanceof Error ? err.message : 'unknown'}`;
|
|
516
|
-
return { kind: 'error', message: run.state.pauseDetail };
|
|
517
|
-
}
|
|
414
|
+
if (Object.keys(variableUpdates).length > 0) {
|
|
415
|
+
fresh.variables = { ...fresh.variables, ...variableUpdates };
|
|
416
|
+
(0, store_1.writeWorkflowData)(run.folderPath, fresh);
|
|
518
417
|
}
|
|
519
418
|
run.state.status = 'running';
|
|
520
419
|
run.state.pauseReason = null;
|
|
@@ -523,7 +422,7 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
523
422
|
log.info({
|
|
524
423
|
projectId: run.projectId,
|
|
525
424
|
nodeId: node.id,
|
|
526
|
-
|
|
425
|
+
wroteVariables: Object.keys(variableUpdates),
|
|
527
426
|
next: node.next,
|
|
528
427
|
}, 'flow node user-input completed');
|
|
529
428
|
return { kind: 'ok', next: node.next };
|
|
@@ -532,68 +431,37 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
532
431
|
if (!this.injector) {
|
|
533
432
|
return { kind: 'error', message: 'no prompt injector configured' };
|
|
534
433
|
}
|
|
535
|
-
// 1.
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
log.warn({
|
|
539
|
-
projectId: run.projectId,
|
|
540
|
-
nodeId: node.id,
|
|
541
|
-
failingProvider: readResult.failingProvider,
|
|
542
|
-
error: readResult.error,
|
|
543
|
-
inputs: node.inputs.map((i) => i.path),
|
|
544
|
-
}, 'flow node llm input read failed');
|
|
545
|
-
if (readResult.failingProvider === 'user') {
|
|
546
|
-
run.state.status = 'paused';
|
|
547
|
-
run.state.pauseReason = 'user-file-read-error';
|
|
548
|
-
run.state.pauseDetail = `failed to read input file (provider=user): ${readResult.error}`;
|
|
549
|
-
this.emit('error', {
|
|
550
|
-
projectId: run.projectId,
|
|
551
|
-
nodeId: node.id,
|
|
552
|
-
reason: 'user-file-read-error',
|
|
553
|
-
detail: run.state.pauseDetail,
|
|
554
|
-
});
|
|
555
|
-
return { kind: 'pause' };
|
|
556
|
-
}
|
|
557
|
-
// provider=llm or system — stash error to inject in the next prompt to
|
|
558
|
-
// the LLM. Since the input was supposedly produced by an upstream LLM
|
|
559
|
-
// node, we still send the prompt to *this* node's LLM but with an
|
|
560
|
-
// explanatory wrapper, asking it to handle/repair.
|
|
561
|
-
run.pendingLlmError = {
|
|
562
|
-
path: node.inputs.find((i) => i.provider !== 'user')?.path ?? '?',
|
|
563
|
-
error: readResult.error ?? 'unknown',
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
// 2. task_todo entry
|
|
567
|
-
const taskIndex = (0, store_1.appendTaskTodo)(run.folderPath, {
|
|
568
|
-
id: node.id,
|
|
434
|
+
// 1. Append a task_progress entry for the LLM to flip when done.
|
|
435
|
+
const taskIndex = (0, store_1.appendTaskProgress)(run.folderPath, {
|
|
436
|
+
nodeId: node.id,
|
|
569
437
|
name: node.name,
|
|
570
438
|
finish: false,
|
|
571
439
|
});
|
|
572
440
|
run.currentTaskIndex = taskIndex;
|
|
573
|
-
//
|
|
574
|
-
const
|
|
575
|
-
? `\n\n[文件读取错误] 上游产物 ${run.pendingLlmError.path} 解析失败:${run.pendingLlmError.error}\n请先修复该文件再继续本任务。\n`
|
|
576
|
-
: '';
|
|
577
|
-
run.pendingLlmError = null;
|
|
441
|
+
// 2. Build prompt: header + read-context blocks + body + write block.
|
|
442
|
+
const data = (0, store_1.readWorkflowData)(run.folderPath);
|
|
578
443
|
const taskHeader = `当前任务 id=${node.id},名为「${node.name}」。\n` +
|
|
579
|
-
`完成后请把 .ccweb/
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
const
|
|
585
|
-
|
|
444
|
+
`完成后请把 .ccweb/workflow_data.json 中 task_progress[${taskIndex}].finish 改为 true(用 Edit/Write 工具直接更新该 JSON 文件)。\n` +
|
|
445
|
+
'保留 workflow_data.json 中其他字段不要动。\n' +
|
|
446
|
+
'\n──────── 任务正文 ────────\n';
|
|
447
|
+
const refConstBlock = buildReadConstBlock(node.readConstants ?? [], run.flowDef, data);
|
|
448
|
+
const refVarBlock = buildReadVarBlock(node.readVariables ?? [], run.flowDef, data);
|
|
449
|
+
const body = renderTemplate(node.promptTemplate, data);
|
|
450
|
+
const writeVarBlock = buildWriteVarBlock(node.writeVariables ?? [], run.flowDef);
|
|
451
|
+
const fullPrompt = `${taskHeader}${refConstBlock}${refVarBlock}${body}${writeVarBlock}`;
|
|
452
|
+
// 3. Inject into chat.
|
|
586
453
|
this.injector(run.projectId, buildPaste(fullPrompt));
|
|
587
454
|
log.info({
|
|
588
455
|
projectId: run.projectId,
|
|
589
456
|
nodeId: node.id,
|
|
590
457
|
taskIndex,
|
|
591
458
|
promptSize: fullPrompt.length,
|
|
592
|
-
|
|
459
|
+
readVariables: node.readVariables ?? [],
|
|
460
|
+
readConstants: node.readConstants ?? [],
|
|
461
|
+
writeVariables: node.writeVariables ?? [],
|
|
593
462
|
timeoutSec: node.timeoutSec,
|
|
594
|
-
inputs: node.inputs.map((i) => i.path),
|
|
595
463
|
}, 'flow node llm prompt injected');
|
|
596
|
-
//
|
|
464
|
+
// 4. Wait for task_progress[taskIndex].finish OR timeout.
|
|
597
465
|
const outcome = await this.waitForTaskFinish(run, taskIndex, node.timeoutSec * 1000);
|
|
598
466
|
log.info({ projectId: run.projectId, nodeId: node.id, taskIndex, outcome }, 'flow node llm wait outcome');
|
|
599
467
|
if (outcome === 'aborted' || outcome === 'paused') {
|
|
@@ -612,87 +480,33 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
612
480
|
log.warn({ projectId: run.projectId, nodeId: node.id, timeoutSec: node.timeoutSec, taskIndex }, 'flow node llm timeout');
|
|
613
481
|
return { kind: 'pause' };
|
|
614
482
|
}
|
|
615
|
-
//
|
|
483
|
+
// Intentionally do NOT write finishedAt back to workflow_data here:
|
|
484
|
+
// the LLM may keep editing variables briefly after flipping finish=true,
|
|
485
|
+
// and our whole-file RMW would silently drop those edits (codex P1-B).
|
|
486
|
+
// Duration audit lives in FlowState.history (saved by runLoop), which
|
|
487
|
+
// is runner-owned so there's no concurrent writer.
|
|
616
488
|
run.currentTaskIndex = null;
|
|
617
489
|
return { kind: 'ok', next: node.next };
|
|
618
490
|
}
|
|
619
491
|
async executeSystemLogic(run, node) {
|
|
620
|
-
|
|
621
|
-
// (resolve via flow.variables → file+field) or field-mode (legacy:
|
|
622
|
-
// node.inputs[0] + branch.field). Files are cached per evaluation pass
|
|
623
|
-
// so multi-branch flows touching the same file pay one read each.
|
|
624
|
-
const variables = run.flowDef.variables ?? [];
|
|
625
|
-
const varByName = new Map(variables.map((v) => [v.name, v]));
|
|
626
|
-
const fileCache = new Map();
|
|
627
|
-
/** Helper: read+parse a file (provider-aware on read error → pause).
|
|
628
|
-
* Returns null when an error has been recorded and caller should
|
|
629
|
-
* return pause; otherwise returns the parsed object. */
|
|
630
|
-
const readFileForBranch = (relPath, provider) => {
|
|
631
|
-
const cached = fileCache.get(relPath);
|
|
632
|
-
if (cached)
|
|
633
|
-
return cached;
|
|
634
|
-
const abs = (0, store_1.safeProjectPath)(run.folderPath, relPath);
|
|
635
|
-
if (!abs) {
|
|
636
|
-
run.state.status = 'paused';
|
|
637
|
-
run.state.pauseReason = 'user-file-read-error';
|
|
638
|
-
run.state.pauseDetail = `unsafe input path rejected: ${relPath}`;
|
|
639
|
-
return 'pause';
|
|
640
|
-
}
|
|
641
|
-
try {
|
|
642
|
-
const parsed = JSON.parse(fs.readFileSync(abs, 'utf-8'));
|
|
643
|
-
const obj = (parsed && typeof parsed === 'object') ? parsed : {};
|
|
644
|
-
fileCache.set(relPath, obj);
|
|
645
|
-
return obj;
|
|
646
|
-
}
|
|
647
|
-
catch (err) {
|
|
648
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
649
|
-
log.warn({ projectId: run.projectId, nodeId: node.id, path: relPath, provider, error: msg }, 'flow node system-logic input parse failed');
|
|
650
|
-
run.state.status = 'paused';
|
|
651
|
-
run.state.pauseReason = provider === 'user' ? 'user-file-read-error' : 'llm-file-read-error';
|
|
652
|
-
run.state.pauseDetail = `failed to parse ${relPath} (provider=${provider}): ${msg}`;
|
|
653
|
-
if (provider !== 'user')
|
|
654
|
-
run.pendingLlmError = { path: relPath, error: msg };
|
|
655
|
-
this.emit('error', {
|
|
656
|
-
projectId: run.projectId,
|
|
657
|
-
nodeId: node.id,
|
|
658
|
-
reason: run.state.pauseReason,
|
|
659
|
-
detail: run.state.pauseDetail,
|
|
660
|
-
});
|
|
661
|
-
return 'pause';
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
// Evaluate branches with loose comparison — LLM often writes JSON
|
|
665
|
-
// booleans as strings ("true"/"false") or numbers; branch authors
|
|
666
|
-
// probably configured the typed primitive.
|
|
492
|
+
const data = (0, store_1.readWorkflowData)(run.folderPath);
|
|
667
493
|
let matched = null;
|
|
668
|
-
let matchedActual
|
|
669
|
-
let
|
|
670
|
-
let
|
|
494
|
+
let matchedActual;
|
|
495
|
+
let matchedSource;
|
|
496
|
+
let matchedName;
|
|
671
497
|
for (const rule of node.branches) {
|
|
672
498
|
let actual;
|
|
673
|
-
let
|
|
674
|
-
let
|
|
499
|
+
let source;
|
|
500
|
+
let name;
|
|
675
501
|
if (rule.variable) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const obj = readFileForBranch(v.file, 'llm');
|
|
680
|
-
if (obj === 'pause')
|
|
681
|
-
return { kind: 'pause' };
|
|
682
|
-
actual = obj[v.name];
|
|
683
|
-
sourceFile = v.file;
|
|
684
|
-
sourceField = v.name;
|
|
502
|
+
actual = data.variables[rule.variable];
|
|
503
|
+
source = 'variable';
|
|
504
|
+
name = rule.variable;
|
|
685
505
|
}
|
|
686
|
-
else if (rule.
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const obj = readFileForBranch(legacyInp.path, legacyInp.provider);
|
|
691
|
-
if (obj === 'pause')
|
|
692
|
-
return { kind: 'pause' };
|
|
693
|
-
actual = obj[rule.field];
|
|
694
|
-
sourceFile = legacyInp.path;
|
|
695
|
-
sourceField = rule.field;
|
|
506
|
+
else if (rule.constant) {
|
|
507
|
+
actual = data.constants[rule.constant];
|
|
508
|
+
source = 'constant';
|
|
509
|
+
name = rule.constant;
|
|
696
510
|
}
|
|
697
511
|
else {
|
|
698
512
|
continue;
|
|
@@ -700,8 +514,8 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
700
514
|
if (branchMatches(actual, rule.equals)) {
|
|
701
515
|
matched = rule;
|
|
702
516
|
matchedActual = actual;
|
|
703
|
-
|
|
704
|
-
|
|
517
|
+
matchedSource = source;
|
|
518
|
+
matchedName = name;
|
|
705
519
|
break;
|
|
706
520
|
}
|
|
707
521
|
}
|
|
@@ -709,21 +523,17 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
709
523
|
log.info({
|
|
710
524
|
projectId: run.projectId,
|
|
711
525
|
nodeId: node.id,
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
matchedField: matched?.field,
|
|
715
|
-
matchedSourceFile,
|
|
716
|
-
matchedSourceField,
|
|
526
|
+
matchedSource,
|
|
527
|
+
matchedName,
|
|
717
528
|
matchedEquals: matched ? JSON.stringify(matched.equals) : undefined,
|
|
718
529
|
actualValue: matched ? JSON.stringify(matchedActual) : undefined,
|
|
719
530
|
goto,
|
|
720
531
|
viaDefault: !matched,
|
|
721
532
|
}, 'flow node system-logic branch evaluated');
|
|
722
|
-
if (goto === null)
|
|
533
|
+
if (goto === null) {
|
|
723
534
|
return { kind: 'ok', next: null };
|
|
724
|
-
|
|
725
|
-
//
|
|
726
|
-
// the same id twice in this run = loop edge.
|
|
535
|
+
}
|
|
536
|
+
// Loop edge detection: target id is already in this run's history.
|
|
727
537
|
const visited = new Set(run.state.history.map((h) => h.nodeId));
|
|
728
538
|
const isBackward = visited.has(goto);
|
|
729
539
|
if (isBackward) {
|
|
@@ -747,6 +557,10 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
747
557
|
return { kind: 'ok', next: goto };
|
|
748
558
|
}
|
|
749
559
|
// ── Wait helpers ──────────────────────────────────────────────────────
|
|
560
|
+
/** Watch workflow_data.json for `task_progress[index].finish = true`. The
|
|
561
|
+
* LLM is told to flip this flag when its work is done. Any other write to
|
|
562
|
+
* workflow_data (variable updates, etc.) also triggers the watcher; the
|
|
563
|
+
* 50ms debounce + finish-check makes that cheap. */
|
|
750
564
|
waitForTaskFinish(run, taskIndex, timeoutMs) {
|
|
751
565
|
return new Promise((resolve) => {
|
|
752
566
|
let settled = false;
|
|
@@ -758,17 +572,22 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
758
572
|
resolve(v);
|
|
759
573
|
};
|
|
760
574
|
run.waitResolve = settle;
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
log.info({ projectId: run.projectId, taskIndex, via: 'initial-check' }, 'flow task_todo finish detected');
|
|
766
|
-
settle('finished');
|
|
767
|
-
return;
|
|
575
|
+
const checkFinish = () => {
|
|
576
|
+
try {
|
|
577
|
+
const d = (0, store_1.readWorkflowData)(run.folderPath);
|
|
578
|
+
return d.task_progress[taskIndex]?.finish === true;
|
|
768
579
|
}
|
|
580
|
+
catch {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
// Initial check — finish may have raced ahead before we attached.
|
|
585
|
+
if (checkFinish()) {
|
|
586
|
+
log.info({ projectId: run.projectId, taskIndex, via: 'initial-check' }, 'flow task_progress finish detected');
|
|
587
|
+
settle('finished');
|
|
588
|
+
return;
|
|
769
589
|
}
|
|
770
|
-
|
|
771
|
-
const filePath = (0, store_1.taskTodoPath)(run.folderPath);
|
|
590
|
+
const filePath = (0, store_1.workflowDataPath)(run.folderPath);
|
|
772
591
|
try {
|
|
773
592
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
774
593
|
}
|
|
@@ -783,34 +602,21 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
783
602
|
run.watcherDebounce = null;
|
|
784
603
|
if (settled)
|
|
785
604
|
return;
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
log.info({ projectId: run.projectId, taskIndex, via: 'watcher' }, 'flow task_todo finish detected');
|
|
790
|
-
settle('finished');
|
|
791
|
-
}
|
|
605
|
+
if (checkFinish()) {
|
|
606
|
+
log.info({ projectId: run.projectId, taskIndex, via: 'watcher' }, 'flow task_progress finish detected');
|
|
607
|
+
settle('finished');
|
|
792
608
|
}
|
|
793
|
-
catch { /* keep waiting */ }
|
|
794
609
|
}, 50);
|
|
795
610
|
});
|
|
796
611
|
}
|
|
797
612
|
catch (err) {
|
|
798
|
-
log.warn({ projectId: run.projectId, err: err instanceof Error ? err.message : String(err) }, '
|
|
799
|
-
// Fallback: 500ms poll
|
|
613
|
+
log.warn({ projectId: run.projectId, err: err instanceof Error ? err.message : String(err) }, 'workflow_data watch failed — falling back to polling');
|
|
800
614
|
const poll = setInterval(() => {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
clearInterval(poll);
|
|
805
|
-
settle('finished');
|
|
806
|
-
}
|
|
615
|
+
if (checkFinish()) {
|
|
616
|
+
clearInterval(poll);
|
|
617
|
+
settle('finished');
|
|
807
618
|
}
|
|
808
|
-
catch { /* ignore */ }
|
|
809
619
|
}, 500);
|
|
810
|
-
// Tie poll to settle: we can't directly cancel here, but settle's
|
|
811
|
-
// clearWaiters won't reach it. Wrap by mirroring as a fake watcher
|
|
812
|
-
// via run.timeoutTimer ergonomics — simpler: stash on run object.
|
|
813
|
-
// For phase-1 acceptable, just accept the small leak past timeout.
|
|
814
620
|
run._poll = poll;
|
|
815
621
|
}
|
|
816
622
|
run.timeoutTimer = setTimeout(() => settle('timeout'), timeoutMs);
|
|
@@ -856,5 +662,8 @@ class FlowRunner extends events_1.EventEmitter {
|
|
|
856
662
|
}
|
|
857
663
|
}
|
|
858
664
|
exports.FlowRunner = FlowRunner;
|
|
665
|
+
// Suppress unused-export warnings — clearFlowState may be used by callers we
|
|
666
|
+
// don't see (e.g. project deletion cleanup).
|
|
667
|
+
void store_1.clearFlowState;
|
|
859
668
|
exports.flowRunner = new FlowRunner();
|
|
860
669
|
//# sourceMappingURL=runner.js.map
|