@runuai/host 0.2.0 → 0.2.2

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.
@@ -278,6 +278,11 @@ export interface SetupTaskDeps {
278
278
  * a failure emits the auth-expired note but never aborts task-up. Returns true
279
279
  * iff a token was injected.
280
280
  */
281
+ /** Tasks whose gh setup is currently in flight. task-up and the first
282
+ * channel-ensure both kick this off and can overlap; a second concurrent
283
+ * `gh auth login` would race the container's gh config, so we dedupe. */
284
+ const setupInFlight = new Set<string>();
285
+
281
286
  export async function setupTaskGithub(
282
287
  taskId: string,
283
288
  userId: string,
@@ -288,6 +293,8 @@ export async function setupTaskGithub(
288
293
  const _inject =
289
294
  deps.inject ?? ((t: string, tok: string) => injectIntoContainer(t, tok));
290
295
  const _schedule = deps.schedule ?? scheduleRefresh;
296
+ if (setupInFlight.has(taskId)) return false;
297
+ setupInFlight.add(taskId);
291
298
  try {
292
299
  if (_hasToken(userId)) {
293
300
  const tok = await _request(userId);
@@ -311,6 +318,8 @@ export async function setupTaskGithub(
311
318
  console.warn(`[github] task ${taskId}: gh setup failed: ${reason}`);
312
319
  authExpiredHandler?.(taskId, userId, reason);
313
320
  return false;
321
+ } finally {
322
+ setupInFlight.delete(taskId);
314
323
  }
315
324
  }
316
325
 
@@ -181,6 +181,16 @@ class Orchestrator {
181
181
  // container), using the creator's per-user key. Best-effort.
182
182
  setupTaskGitIdentity(channel.taskId, task.ownerName, task.ownerEmail);
183
183
 
184
+ // GitHub auth for `gh` (ADR-027): mint + inject the access token and
185
+ // (re)start its refresh schedule. Done here — not only at task-up — so it
186
+ // survives a host restart: recovery recreates the channel and re-runs
187
+ // ensureSessions, whereas the in-memory refresh timer from the original
188
+ // task-up is gone. Guarded + best-effort + non-blocking; the token lands
189
+ // before the agents (spawned just below) make their first `gh` call.
190
+ if (task.ownerUserId) {
191
+ void setupTaskGithub(channel.taskId, task.ownerUserId);
192
+ }
193
+
184
194
  for (const agent of channel.roster) {
185
195
  const session = await this.factory.create({
186
196
  taskId: channel.taskId,
@@ -365,7 +375,16 @@ class Orchestrator {
365
375
  });
366
376
  break;
367
377
  }
368
- case "turn_complete":
378
+ case "turn_complete": {
379
+ // Turn boundary — the cloud flushes any @-mentions buffered across this
380
+ // turn's messages and wakes the mentioned peers with the full turn.
381
+ this.emitHost({
382
+ kind: "agent.turn_complete",
383
+ taskId: channel.taskId,
384
+ agentId,
385
+ });
386
+ break;
387
+ }
369
388
  case "exit":
370
389
  break;
371
390
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Uai host — runs ephemeral AI coding tasks in Docker on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Diogo Perillo <diogo.perillo@gmail.com>",
package/src/protocol.ts CHANGED
@@ -399,6 +399,15 @@ export type HostEvent =
399
399
  fullText: string;
400
400
  mentions: string[];
401
401
  }
402
+ // The agent finished a turn (it may have emitted several message_complete
403
+ // items within it). The cloud waits for this before waking @-mentioned peers,
404
+ // so a peer is handed the agent's COMPLETE turn rather than a mid-turn
405
+ // fragment that happened to contain the mention.
406
+ | {
407
+ kind: "agent.turn_complete";
408
+ taskId: string;
409
+ agentId: string;
410
+ }
402
411
  | {
403
412
  kind: "agent.tool_call";
404
413
  taskId: string;