orchestrar 0.3.2 → 0.3.3

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/orchestrator.js +105 -16
  2. package/package.json +1 -1
package/orchestrator.js CHANGED
@@ -50,23 +50,23 @@ async function runWorkInstance(createOpencode, docs, root) {
50
50
  const sessionID = extractSessionID(session, "session.create (work instance)");
51
51
  const promptPaths = buildPromptPaths(docs, root);
52
52
 
53
- await sendPrompt(
53
+ const workMessageID = await sendPrompt(
54
54
  client,
55
55
  sessionID,
56
56
  buildMilestonePrompt(promptPaths),
57
57
  root
58
58
  );
59
- await waitForSessionIdle(client, sessionID, root);
59
+ await waitForMessageComplete(client, sessionID, workMessageID, root);
60
60
 
61
61
  await runReviewLoop(createOpencode, client, sessionID, root);
62
62
 
63
- await sendPrompt(
63
+ const markTasksMessageID = await sendPrompt(
64
64
  client,
65
65
  sessionID,
66
66
  buildMarkTasksPrompt(promptPaths.plan),
67
67
  root
68
68
  );
69
- await waitForSessionIdle(client, sessionID, root);
69
+ await waitForMessageComplete(client, sessionID, markTasksMessageID, root);
70
70
  } finally {
71
71
  await disposeInstance(client, server, root);
72
72
  }
@@ -86,7 +86,7 @@ async function runCommitInstance(createOpencode, root) {
86
86
  );
87
87
 
88
88
  const sessionID = extractSessionID(session, "session.create (commit instance)");
89
- await sendPrompt(
89
+ const commitMessageID = await sendPrompt(
90
90
  client,
91
91
  sessionID,
92
92
  buildCommitPrompt(),
@@ -94,7 +94,7 @@ async function runCommitInstance(createOpencode, root) {
94
94
  COMMIT_MODEL,
95
95
  COMMIT_AGENT
96
96
  );
97
- await waitForSessionIdle(client, sessionID, root);
97
+ await waitForMessageComplete(client, sessionID, commitMessageID, root);
98
98
  } finally {
99
99
  await disposeInstance(client, server, root);
100
100
  }
@@ -118,13 +118,13 @@ async function runReviewLoop(createOpencode, client, sessionID, root) {
118
118
  ? reviewResult.findings.length
119
119
  : "unknown";
120
120
  logStep(`Review found ${findingsCount} issues; requesting fixes.`);
121
- await sendPrompt(
121
+ const fixMessageID = await sendPrompt(
122
122
  client,
123
123
  sessionID,
124
124
  buildFindingsPrompt(reviewResult),
125
125
  root
126
126
  );
127
- await waitForSessionIdle(client, sessionID, root);
127
+ await waitForMessageComplete(client, sessionID, fixMessageID, root);
128
128
  }
129
129
 
130
130
  throw new Error(
@@ -171,10 +171,17 @@ async function runReviewCommand(createOpencode, root) {
171
171
  "session.command"
172
172
  );
173
173
 
174
- await waitForSessionIdle(client, sessionID, root, timeoutMs);
174
+ const commandMessageID = extractMessageID(commandResult);
175
+ await waitForMessageComplete(
176
+ client,
177
+ sessionID,
178
+ commandMessageID,
179
+ root,
180
+ timeoutMs
181
+ );
175
182
 
176
183
  let parts = commandResult?.parts ?? [];
177
- const messageID = commandResult?.info?.id;
184
+ const messageID = extractMessageID(commandResult);
178
185
  if (messageID) {
179
186
  const message = await unwrap(
180
187
  client.session.message({
@@ -205,7 +212,7 @@ async function sendPrompt(
205
212
  agentSpec = DEFAULT_AGENT
206
213
  ) {
207
214
  const model = parseModelSpec(modelSpec);
208
- await unwrap(
215
+ const response = await unwrap(
209
216
  client.session.prompt({
210
217
  path: { id: sessionID },
211
218
  query: { directory: root },
@@ -217,6 +224,8 @@ async function sendPrompt(
217
224
  }),
218
225
  "session.prompt"
219
226
  );
227
+
228
+ return extractMessageID(response);
220
229
  }
221
230
 
222
231
  async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
@@ -232,26 +241,79 @@ async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
232
241
  );
233
242
 
234
243
  const start = Date.now();
244
+ let lastKnownSessions = [];
235
245
  while (Date.now() - start < timeoutMs) {
236
246
  const statusMap = await unwrap(
237
247
  client.session.status({ query: { directory: root } }),
238
248
  "session.status"
239
249
  );
250
+ if (statusMap && typeof statusMap === "object") {
251
+ lastKnownSessions = Object.keys(statusMap);
252
+ }
240
253
  const status = statusMap?.[sessionID];
241
254
  if (!status) {
242
- const knownSessions = statusMap ? Object.keys(statusMap) : [];
243
- const knownList = knownSessions.length ? knownSessions.join(", ") : "none";
255
+ await delay(pollIntervalMs);
256
+ continue;
257
+ }
258
+ if (status.type === "idle") {
259
+ return;
260
+ }
261
+ await delay(pollIntervalMs);
262
+ }
263
+
264
+ const knownList = lastKnownSessions.length
265
+ ? lastKnownSessions.join(", ")
266
+ : "none";
267
+ throw new Error(
268
+ `Timed out waiting for session ${sessionID} to go idle. Known sessions: ${knownList}.`
269
+ );
270
+ }
271
+
272
+ async function waitForMessageComplete(
273
+ client,
274
+ sessionID,
275
+ messageID,
276
+ root,
277
+ timeoutOverrideMs
278
+ ) {
279
+ if (!messageID) {
280
+ await waitForSessionIdle(client, sessionID, root, timeoutOverrideMs);
281
+ return;
282
+ }
283
+
284
+ const timeoutMs =
285
+ timeoutOverrideMs ??
286
+ parseNumber(
287
+ process.env.ORCHESTRATOR_SESSION_TIMEOUT_MS,
288
+ DEFAULT_SESSION_TIMEOUT_MS
289
+ );
290
+ const pollIntervalMs = parseNumber(
291
+ process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
292
+ DEFAULT_STATUS_POLL_INTERVAL_MS
293
+ );
294
+
295
+ const start = Date.now();
296
+ while (Date.now() - start < timeoutMs) {
297
+ const message = await unwrap(
298
+ client.session.message({
299
+ path: { id: sessionID, messageID },
300
+ query: { directory: root },
301
+ }),
302
+ "session.message"
303
+ );
304
+ const info = message?.info ?? message;
305
+ if (info?.error) {
244
306
  throw new Error(
245
- `Session status missing for ${sessionID}. Known sessions: ${knownList}.`
307
+ `Session message ${messageID} failed: ${formatError(info.error)}`
246
308
  );
247
309
  }
248
- if (status.type === "idle") {
310
+ if (info?.time?.completed || info?.finish) {
249
311
  return;
250
312
  }
251
313
  await delay(pollIntervalMs);
252
314
  }
253
315
 
254
- throw new Error(`Timed out waiting for session ${sessionID} to go idle.`);
316
+ throw new Error(`Timed out waiting for message ${messageID} to complete.`);
255
317
  }
256
318
 
257
319
  async function resolveDocs(root) {
@@ -506,6 +568,33 @@ function extractSessionID(session, context) {
506
568
  );
507
569
  }
508
570
 
571
+ function extractMessageID(message) {
572
+ if (!message || typeof message !== "object") {
573
+ return;
574
+ }
575
+
576
+ const candidates = [
577
+ message.info?.id,
578
+ message.info?.messageID,
579
+ message.id,
580
+ message.messageID,
581
+ message.data?.info?.id,
582
+ message.data?.info?.messageID,
583
+ message.data?.id,
584
+ message.data?.messageID,
585
+ message.properties?.info?.id,
586
+ message.properties?.info?.messageID,
587
+ message.properties?.id,
588
+ message.properties?.messageID,
589
+ ];
590
+
591
+ for (const candidate of candidates) {
592
+ if (typeof candidate === "string" && candidate.trim()) {
593
+ return candidate;
594
+ }
595
+ }
596
+ }
597
+
509
598
  function safeStringify(value, maxLength = 1000) {
510
599
  try {
511
600
  const json = JSON.stringify(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrar",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "OpenCode milestone orchestrator",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",