orquesta-cli 0.2.101 → 0.2.103

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.
@@ -221,6 +221,7 @@ export class LLMClient {
221
221
  });
222
222
  const url = '/chat/completions';
223
223
  let controller = null;
224
+ let gapTimedOut = false;
224
225
  try {
225
226
  logger.flow('Starting message preprocessing');
226
227
  const modelId = options.model || this.model;
@@ -272,61 +273,80 @@ export class LLMClient {
272
273
  const toolCallsMap = new Map();
273
274
  let responseId = '';
274
275
  let responseModel = '';
275
- for await (const chunk of stream) {
276
- if (this.isInterrupted) {
277
- throw new Error('INTERRUPTED');
278
- }
279
- buffer += chunk.toString();
280
- const lines = buffer.split('\n');
281
- buffer = lines.pop() || '';
282
- for (const line of lines) {
283
- const trimmed = line.trim();
284
- if (!trimmed || trimmed === 'data: [DONE]')
285
- continue;
286
- if (!trimmed.startsWith('data: '))
287
- continue;
276
+ const GAP_MS = Number(process.env['ORQUESTA_STREAM_GAP_MS']) || 45000;
277
+ let lastChunkAt = Date.now();
278
+ const gapTimer = setInterval(() => {
279
+ if (Date.now() - lastChunkAt > GAP_MS) {
280
+ gapTimedOut = true;
281
+ clearInterval(gapTimer);
288
282
  try {
289
- const data = JSON.parse(trimmed.slice(6));
290
- if (data.id)
291
- responseId = data.id;
292
- if (data.model)
293
- responseModel = data.model;
294
- const choice = data.choices?.[0];
295
- if (!choice)
283
+ controller?.abort();
284
+ }
285
+ catch { }
286
+ }
287
+ }, 5000);
288
+ gapTimer.unref?.();
289
+ try {
290
+ for await (const chunk of stream) {
291
+ lastChunkAt = Date.now();
292
+ if (this.isInterrupted) {
293
+ throw new Error('INTERRUPTED');
294
+ }
295
+ buffer += chunk.toString();
296
+ const lines = buffer.split('\n');
297
+ buffer = lines.pop() || '';
298
+ for (const line of lines) {
299
+ const trimmed = line.trim();
300
+ if (!trimmed || trimmed === 'data: [DONE]')
296
301
  continue;
297
- if (choice.finish_reason)
298
- finishReason = choice.finish_reason;
299
- const delta = choice.delta;
300
- if (!delta)
302
+ if (!trimmed.startsWith('data: '))
301
303
  continue;
302
- if (delta.role)
303
- role = delta.role;
304
- if (delta.content) {
305
- contentAccum += delta.content;
306
- this.onStreamingContent(delta.content);
307
- }
308
- if (delta.reasoning) {
309
- reasoningAccum += delta.reasoning;
310
- }
311
- if (delta.tool_calls) {
312
- for (const tc of delta.tool_calls) {
313
- const idx = tc.index ?? 0;
314
- if (!toolCallsMap.has(idx)) {
315
- toolCallsMap.set(idx, { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } });
304
+ try {
305
+ const data = JSON.parse(trimmed.slice(6));
306
+ if (data.id)
307
+ responseId = data.id;
308
+ if (data.model)
309
+ responseModel = data.model;
310
+ const choice = data.choices?.[0];
311
+ if (!choice)
312
+ continue;
313
+ if (choice.finish_reason)
314
+ finishReason = choice.finish_reason;
315
+ const delta = choice.delta;
316
+ if (!delta)
317
+ continue;
318
+ if (delta.role)
319
+ role = delta.role;
320
+ if (delta.content) {
321
+ contentAccum += delta.content;
322
+ this.onStreamingContent(delta.content);
323
+ }
324
+ if (delta.reasoning) {
325
+ reasoningAccum += delta.reasoning;
326
+ }
327
+ if (delta.tool_calls) {
328
+ for (const tc of delta.tool_calls) {
329
+ const idx = tc.index ?? 0;
330
+ if (!toolCallsMap.has(idx)) {
331
+ toolCallsMap.set(idx, { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } });
332
+ }
333
+ const existing = toolCallsMap.get(idx);
334
+ if (tc.id)
335
+ existing.id = tc.id;
336
+ if (tc.function?.name)
337
+ existing.function.name += tc.function.name;
338
+ if (tc.function?.arguments)
339
+ existing.function.arguments += tc.function.arguments;
316
340
  }
317
- const existing = toolCallsMap.get(idx);
318
- if (tc.id)
319
- existing.id = tc.id;
320
- if (tc.function?.name)
321
- existing.function.name += tc.function.name;
322
- if (tc.function?.arguments)
323
- existing.function.arguments += tc.function.arguments;
324
341
  }
325
342
  }
343
+ catch { }
326
344
  }
327
- catch { }
328
345
  }
329
346
  }
347
+ finally {
348
+ clearInterval(gapTimer);
349
+ }
330
350
  const toolCalls = Array.from(toolCallsMap.values())
331
351
  .filter(tc => tc.id && tc.function.name)
332
352
  .map(tc => ({ id: tc.id, type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments } }));
@@ -397,6 +417,11 @@ export class LLMClient {
397
417
  return response.data;
398
418
  }
399
419
  catch (error) {
420
+ if (gapTimedOut) {
421
+ logger.flow('Stream stalled — gap watchdog aborted the request');
422
+ logger.exit('chatCompletion', { success: false, stalled: true });
423
+ throw new Error('Upstream stalled: no streaming data received within the gap window. Try again or switch endpoint.');
424
+ }
400
425
  if (axios.isCancel(error) || (error instanceof Error && error.name === 'CanceledError')) {
401
426
  logger.flow('API call canceled (user interrupt)');
402
427
  logger.exit('chatCompletion', { success: false, aborted: true });
@@ -47,10 +47,13 @@ export function planWaves(todos) {
47
47
  }
48
48
  function buildWorkerMessages(ctx) {
49
49
  const upstreamSection = Object.keys(ctx.upstreamOutputs).length > 0
50
- ? `\n\nUpstream task outputs (your dependencies):\n${Object.entries(ctx.upstreamOutputs)
50
+ ? `\n\nFindings from already-completed tasks (REUSE these — do not re-discover what they already found, e.g. file locations):\n${Object.entries(ctx.upstreamOutputs)
51
51
  .map(([id, out]) => `- [${id}] ${truncate(out, 800)}`)
52
52
  .join('\n')}`
53
53
  : '';
54
+ const siblingSection = ctx.siblingTitles && ctx.siblingTitles.length > 0
55
+ ? `\n\nRunning in parallel RIGHT NOW (other workers own these — do NOT do their work, and avoid redundant whole-repo scans they'd also run):\n${ctx.siblingTitles.map(t => `- ${t}`).join('\n')}`
56
+ : '';
54
57
  const cwdNote = ctx.workingDirectory && ctx.workingDirectory !== process.cwd()
55
58
  ? `\n\nIMPORTANT: Run all filesystem and shell operations from this directory: ${ctx.workingDirectory}\nDO NOT cd elsewhere.`
56
59
  : '';
@@ -60,7 +63,7 @@ function buildWorkerMessages(ctx) {
60
63
  role: 'user',
61
64
  content: `Execute ONLY this single task and then call final_response with a short summary of what you did. Do not start other tasks.
62
65
 
63
- Task [${ctx.todo.id}]: ${ctx.todo.title}${upstreamSection}${cwdNote}`,
66
+ Task [${ctx.todo.id}]: ${ctx.todo.title}${upstreamSection}${siblingSection}${cwdNote}`,
64
67
  },
65
68
  ];
66
69
  }
@@ -197,17 +200,15 @@ export async function runParallelGraph(opts) {
197
200
  }
198
201
  }
199
202
  }
203
+ const priorFindings = { ...outputs };
200
204
  const settled = await Promise.allSettled(batch.map(t => runWorker({
201
205
  todo: todoById.get(t.id),
202
206
  sessionId,
203
207
  llmClient,
204
208
  baseSystemPrompt,
205
209
  executorModel,
206
- upstreamOutputs: (t.dependsOn ?? []).reduce((acc, depId) => {
207
- if (outputs[depId])
208
- acc[depId] = outputs[depId];
209
- return acc;
210
- }, {}),
210
+ upstreamOutputs: priorFindings,
211
+ siblingTitles: batch.filter(s => s.id !== t.id).map(s => todoById.get(s.id).title),
211
212
  workingDirectory: allocations.get(t.id),
212
213
  })));
213
214
  for (let k = 0; k < settled.length; k++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquesta-cli",
3
- "version": "0.2.101",
3
+ "version": "0.2.103",
4
4
  "description": "Orquesta CLI - AI-powered coding assistant with team collaboration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",