orchestrar 0.3.2 → 0.3.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.
Files changed (2) hide show
  1. package/orchestrator.js +142 -62
  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
- await runReviewLoop(createOpencode, client, sessionID, root);
61
+ await runReviewLoop(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,13 +94,13 @@ 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
  }
101
101
  }
102
102
 
103
- async function runReviewLoop(createOpencode, client, sessionID, root) {
103
+ async function runReviewLoop(client, sessionID, root) {
104
104
  const maxIterations = parseNumber(
105
105
  process.env.ORCHESTRATOR_MAX_REVIEW_ITERATIONS,
106
106
  DEFAULT_MAX_REVIEW_ITERATIONS
@@ -108,7 +108,7 @@ async function runReviewLoop(createOpencode, client, sessionID, root) {
108
108
 
109
109
  for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
110
110
  logStep(`Running review (${iteration}/${maxIterations})`);
111
- const reviewResult = await runReviewCommand(createOpencode, root);
111
+ const reviewResult = await runReviewCommand(client, root);
112
112
  if (isFindingsEmpty(reviewResult)) {
113
113
  logStep("Review clean; no findings.");
114
114
  return;
@@ -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(
@@ -132,7 +132,7 @@ async function runReviewLoop(createOpencode, client, sessionID, root) {
132
132
  );
133
133
  }
134
134
 
135
- async function runReviewCommand(createOpencode, root) {
135
+ async function runReviewCommand(client, root) {
136
136
  const commandName =
137
137
  process.env.ORCHESTRATOR_REVIEW_COMMAND || DEFAULT_REVIEW_COMMAND_NAME;
138
138
  const commandArguments =
@@ -142,58 +142,56 @@ async function runReviewCommand(createOpencode, root) {
142
142
  process.env.ORCHESTRATOR_REVIEW_TIMEOUT_MS,
143
143
  DEFAULT_REVIEW_TIMEOUT_MS
144
144
  );
145
+ const session = await unwrap(
146
+ client.session.create({
147
+ query: { directory: root },
148
+ body: { title: "Review" },
149
+ }),
150
+ "session.create"
151
+ );
145
152
 
146
- const { client, server } = await createOpencode({
147
- config: buildConfig(DEFAULT_MODEL),
148
- });
153
+ const sessionID = extractSessionID(session, "session.create (review instance)");
154
+ const commandResult = await unwrap(
155
+ client.session.command({
156
+ path: { id: sessionID },
157
+ query: { directory: root },
158
+ body: {
159
+ command: commandName,
160
+ arguments: commandArguments,
161
+ agent: DEFAULT_AGENT,
162
+ model: DEFAULT_MODEL,
163
+ },
164
+ }),
165
+ "session.command"
166
+ );
149
167
 
150
- try {
151
- const session = await unwrap(
152
- client.session.create({
153
- query: { directory: root },
154
- body: { title: "Review" },
155
- }),
156
- "session.create"
157
- );
168
+ const commandMessageID = extractMessageID(commandResult);
169
+ await waitForMessageComplete(
170
+ client,
171
+ sessionID,
172
+ commandMessageID,
173
+ root,
174
+ timeoutMs
175
+ );
158
176
 
159
- const sessionID = extractSessionID(session, "session.create (review instance)");
160
- const commandResult = await unwrap(
161
- client.session.command({
162
- path: { id: sessionID },
177
+ let parts = commandResult?.parts ?? [];
178
+ const messageID = extractMessageID(commandResult);
179
+ if (messageID) {
180
+ const message = await unwrap(
181
+ client.session.message({
182
+ path: { id: sessionID, messageID },
163
183
  query: { directory: root },
164
- body: {
165
- command: commandName,
166
- arguments: commandArguments,
167
- agent: DEFAULT_AGENT,
168
- model: DEFAULT_MODEL,
169
- },
170
184
  }),
171
- "session.command"
185
+ "session.message"
172
186
  );
187
+ parts = message?.parts ?? parts;
188
+ }
173
189
 
174
- await waitForSessionIdle(client, sessionID, root, timeoutMs);
175
-
176
- let parts = commandResult?.parts ?? [];
177
- const messageID = commandResult?.info?.id;
178
- if (messageID) {
179
- const message = await unwrap(
180
- client.session.message({
181
- path: { id: sessionID, messageID },
182
- query: { directory: root },
183
- }),
184
- "session.message"
185
- );
186
- parts = message?.parts ?? parts;
187
- }
188
-
189
- const output = collectCommandOutput(parts);
190
- if (!output.trim()) {
191
- throw new Error("Review command produced no output.");
192
- }
193
- return extractReviewJson(output);
194
- } finally {
195
- await disposeInstance(client, server, root);
190
+ const output = collectCommandOutput(parts);
191
+ if (!output.trim()) {
192
+ throw new Error("Review command produced no output.");
196
193
  }
194
+ return extractReviewJson(output);
197
195
  }
198
196
 
199
197
  async function sendPrompt(
@@ -205,7 +203,7 @@ async function sendPrompt(
205
203
  agentSpec = DEFAULT_AGENT
206
204
  ) {
207
205
  const model = parseModelSpec(modelSpec);
208
- await unwrap(
206
+ const response = await unwrap(
209
207
  client.session.prompt({
210
208
  path: { id: sessionID },
211
209
  query: { directory: root },
@@ -217,6 +215,8 @@ async function sendPrompt(
217
215
  }),
218
216
  "session.prompt"
219
217
  );
218
+
219
+ return extractMessageID(response);
220
220
  }
221
221
 
222
222
  async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
@@ -232,26 +232,79 @@ async function waitForSessionIdle(client, sessionID, root, timeoutOverrideMs) {
232
232
  );
233
233
 
234
234
  const start = Date.now();
235
+ let lastKnownSessions = [];
235
236
  while (Date.now() - start < timeoutMs) {
236
237
  const statusMap = await unwrap(
237
238
  client.session.status({ query: { directory: root } }),
238
239
  "session.status"
239
240
  );
241
+ if (statusMap && typeof statusMap === "object") {
242
+ lastKnownSessions = Object.keys(statusMap);
243
+ }
240
244
  const status = statusMap?.[sessionID];
241
245
  if (!status) {
242
- const knownSessions = statusMap ? Object.keys(statusMap) : [];
243
- const knownList = knownSessions.length ? knownSessions.join(", ") : "none";
246
+ await delay(pollIntervalMs);
247
+ continue;
248
+ }
249
+ if (status.type === "idle") {
250
+ return;
251
+ }
252
+ await delay(pollIntervalMs);
253
+ }
254
+
255
+ const knownList = lastKnownSessions.length
256
+ ? lastKnownSessions.join(", ")
257
+ : "none";
258
+ throw new Error(
259
+ `Timed out waiting for session ${sessionID} to go idle. Known sessions: ${knownList}.`
260
+ );
261
+ }
262
+
263
+ async function waitForMessageComplete(
264
+ client,
265
+ sessionID,
266
+ messageID,
267
+ root,
268
+ timeoutOverrideMs
269
+ ) {
270
+ if (!messageID) {
271
+ await waitForSessionIdle(client, sessionID, root, timeoutOverrideMs);
272
+ return;
273
+ }
274
+
275
+ const timeoutMs =
276
+ timeoutOverrideMs ??
277
+ parseNumber(
278
+ process.env.ORCHESTRATOR_SESSION_TIMEOUT_MS,
279
+ DEFAULT_SESSION_TIMEOUT_MS
280
+ );
281
+ const pollIntervalMs = parseNumber(
282
+ process.env.ORCHESTRATOR_STATUS_POLL_INTERVAL_MS,
283
+ DEFAULT_STATUS_POLL_INTERVAL_MS
284
+ );
285
+
286
+ const start = Date.now();
287
+ while (Date.now() - start < timeoutMs) {
288
+ const message = await unwrap(
289
+ client.session.message({
290
+ path: { id: sessionID, messageID },
291
+ query: { directory: root },
292
+ }),
293
+ "session.message"
294
+ );
295
+ const info = message?.info ?? message;
296
+ if (info?.error) {
244
297
  throw new Error(
245
- `Session status missing for ${sessionID}. Known sessions: ${knownList}.`
298
+ `Session message ${messageID} failed: ${formatError(info.error)}`
246
299
  );
247
300
  }
248
- if (status.type === "idle") {
301
+ if (info?.time?.completed || info?.finish) {
249
302
  return;
250
303
  }
251
304
  await delay(pollIntervalMs);
252
305
  }
253
306
 
254
- throw new Error(`Timed out waiting for session ${sessionID} to go idle.`);
307
+ throw new Error(`Timed out waiting for message ${messageID} to complete.`);
255
308
  }
256
309
 
257
310
  async function resolveDocs(root) {
@@ -506,6 +559,33 @@ function extractSessionID(session, context) {
506
559
  );
507
560
  }
508
561
 
562
+ function extractMessageID(message) {
563
+ if (!message || typeof message !== "object") {
564
+ return;
565
+ }
566
+
567
+ const candidates = [
568
+ message.info?.id,
569
+ message.info?.messageID,
570
+ message.id,
571
+ message.messageID,
572
+ message.data?.info?.id,
573
+ message.data?.info?.messageID,
574
+ message.data?.id,
575
+ message.data?.messageID,
576
+ message.properties?.info?.id,
577
+ message.properties?.info?.messageID,
578
+ message.properties?.id,
579
+ message.properties?.messageID,
580
+ ];
581
+
582
+ for (const candidate of candidates) {
583
+ if (typeof candidate === "string" && candidate.trim()) {
584
+ return candidate;
585
+ }
586
+ }
587
+ }
588
+
509
589
  function safeStringify(value, maxLength = 1000) {
510
590
  try {
511
591
  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.4",
4
4
  "description": "OpenCode milestone orchestrator",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",