@shanesaravia/hive 0.2.1 → 0.4.0
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/CHANGELOG.md +53 -0
- package/README.md +18 -1
- package/node_modules/@hive/shared/dist/directStudio.d.ts +6 -0
- package/node_modules/@hive/shared/dist/directStudio.js +12 -0
- package/node_modules/@hive/shared/dist/index.d.ts +2 -0
- package/node_modules/@hive/shared/dist/index.js +2 -0
- package/node_modules/@hive/shared/dist/reviewHall.d.ts +15 -0
- package/node_modules/@hive/shared/dist/reviewHall.js +49 -0
- package/node_modules/@hive/shared/dist/status.js +9 -0
- package/node_modules/@hive/shared/dist/types.d.ts +191 -1
- package/node_modules/@hive/shared/dist/types.js +27 -0
- package/node_modules/@hive/shared/dist/workers.d.ts +14 -0
- package/node_modules/@hive/shared/dist/workers.js +22 -0
- package/package.json +1 -1
- package/packages/server/dist/agents/agentDiscovery.js +64 -0
- package/packages/server/dist/api/rest.js +538 -23
- package/packages/server/dist/api/ws.js +90 -9
- package/packages/server/dist/control/launcher.js +50 -11
- package/packages/server/dist/control/messaging.js +3 -2
- package/packages/server/dist/control/missionQuiesce.js +66 -0
- package/packages/server/dist/health/deriveAlerts.js +9 -2
- package/packages/server/dist/hooks/hookIngest.js +69 -10
- package/packages/server/dist/index.js +44 -4
- package/packages/server/dist/loops/loopCommand.js +56 -0
- package/packages/server/dist/loops/loopNoop.js +38 -0
- package/packages/server/dist/loops/loopScheduler.js +58 -0
- package/packages/server/dist/loops/loopStore.js +118 -0
- package/packages/server/dist/loops/monitors.js +38 -0
- package/packages/server/dist/messages/attachmentStore.js +92 -0
- package/packages/server/dist/messages/messagesStore.js +110 -33
- package/packages/server/dist/missions/missionsStore.js +9 -0
- package/packages/server/dist/missions/reopenOnWork.js +20 -0
- package/packages/server/dist/plans/planReconcile.js +114 -0
- package/packages/server/dist/plans/plansStore.js +46 -3
- package/packages/server/dist/reviews/reviewDiff.js +47 -0
- package/packages/server/dist/roster/missionReplay.js +82 -0
- package/packages/server/dist/roster/replyAsk.js +62 -0
- package/packages/server/dist/roster/rosterBuilder.js +78 -147
- package/packages/server/dist/roster/workerIdentity.js +923 -0
- package/packages/server/dist/skills/skillDiscovery.js +28 -4
- package/packages/server/dist/terminals/claudeStreamClient.js +90 -0
- package/packages/server/dist/terminals/codexAppServerClient.js +195 -0
- package/packages/server/dist/terminals/providerDetection.js +27 -0
- package/packages/server/dist/terminals/terminalCapability.js +45 -0
- package/packages/server/dist/terminals/terminalFeatures.js +11 -0
- package/packages/server/dist/terminals/terminalObservability.js +21 -0
- package/packages/server/dist/terminals/terminalRuntime.js +125 -0
- package/packages/server/dist/terminals/terminalStream.js +30 -0
- package/packages/server/dist/transcripts/transcriptReader.js +345 -0
- package/packages/server/dist/watch/jobsWatcher.js +55 -24
- package/packages/web/dist/assets/index-DWjqiitn.js +17 -0
- package/packages/web/dist/assets/index-rd4RnLqj.css +2 -0
- package/packages/web/dist/index.html +2 -2
- package/templates/agents/hive-orchestrator.md +1 -0
- package/packages/web/dist/assets/index-Bzle5Xla.css +0 -2
- package/packages/web/dist/assets/index-C6AY0vYC.js +0 -11
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { deriveStatus, } from "@hive/shared";
|
|
2
2
|
import { allPlanTasks } from "../plans/plansStore.js";
|
|
3
|
+
import { deriveChangedFiles, reconcilePlanWorkers } from "../plans/planReconcile.js";
|
|
3
4
|
import { config } from "../config.js";
|
|
4
5
|
import { deriveAlerts } from "../health/deriveAlerts.js";
|
|
6
|
+
import { jobIsLive } from "../control/missionQuiesce.js";
|
|
7
|
+
import { resolveMissionWorkers } from "./workerIdentity.js";
|
|
8
|
+
import { detectReplyAsk } from "./replyAsk.js";
|
|
5
9
|
const ORCHESTRATOR_AGENT_NAME = "hive-orchestrator";
|
|
6
10
|
function dateMs(value) {
|
|
7
11
|
const parsed = value ? Date.parse(value) : NaN;
|
|
@@ -18,7 +22,7 @@ function resultText(job) {
|
|
|
18
22
|
return undefined;
|
|
19
23
|
}
|
|
20
24
|
/** Joins Claude's short-lived jobs into durable, Hive-managed missions. */
|
|
21
|
-
export function buildFleetSnapshot(sessions, jobs, events, missions, messageStore, planStore, now = Date.now()) {
|
|
25
|
+
export function buildFleetSnapshot(sessions, jobs, events, missions, messageStore, planStore, now = Date.now(), workerIdentity) {
|
|
22
26
|
const workerCorrelationGraceMs = 8_000;
|
|
23
27
|
const orchestrators = [];
|
|
24
28
|
const other = [];
|
|
@@ -95,7 +99,20 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
95
99
|
choices: latestJob.promptChoices ?? [], impact: "The provider is waiting for this answer before the skill can continue.", createdAt: node.updatedAt, ...missionContext,
|
|
96
100
|
}]
|
|
97
101
|
: [];
|
|
98
|
-
|
|
102
|
+
// A turn that ended by asking the reader something. No structured signal
|
|
103
|
+
// fires for this — the agent simply wrote the question — so without it the
|
|
104
|
+
// mission sits looking finished while it waits for an answer.
|
|
105
|
+
const replyAsk = node.turnCompleted && latestJob && !resolved.has(`ask:${node.jobId}`)
|
|
106
|
+
? detectReplyAsk(latestJob.lastText ?? resultText(latestJob))
|
|
107
|
+
: undefined;
|
|
108
|
+
const fromReply = replyAsk && !fromEvents.length && !parked.length && !providerPrompt.length
|
|
109
|
+
? [{
|
|
110
|
+
id: `ask:${node.jobId}`, missionId: node.missionId, missionName: node.name,
|
|
111
|
+
kind: "question", question: replyAsk.question,
|
|
112
|
+
choices: [], impact: "The mission has finished its turn and is waiting on your answer.", createdAt: node.updatedAt, ...missionContext,
|
|
113
|
+
}]
|
|
114
|
+
: [];
|
|
115
|
+
return [...fromEvents, ...parked, ...providerPrompt, ...fromReply];
|
|
99
116
|
};
|
|
100
117
|
for (const [missionId, group] of groups) {
|
|
101
118
|
group.sort((a, b) => dateMs(a.job.createdAt) - dateMs(b.job.createdAt));
|
|
@@ -124,7 +141,7 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
124
141
|
.map((event) => [`${event.sessionId}:${event.jobId ?? ""}:${event.ts}:${event.phase}:${event.decisionId ?? ""}:${event.targetWorker ?? ""}:${event.detail}`, event])).values()]
|
|
125
142
|
.sort((a, b) => a.ts - b.ts);
|
|
126
143
|
let recentEvents = allEvents.slice(-100);
|
|
127
|
-
|
|
144
|
+
const fanWorkers = group.flatMap(({ jobId, job }) => (job.fan ?? []).filter((worker) => worker.kind === "agent" || worker.kind === "subagent").map((worker) => ({
|
|
128
145
|
...worker,
|
|
129
146
|
jobId,
|
|
130
147
|
jobState: job.state,
|
|
@@ -134,150 +151,48 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
134
151
|
const linkedSession = sessionsByJob.get(jobId);
|
|
135
152
|
return [...(job.sessionId ? [[job.sessionId, jobId]] : []), ...(linkedSession ? [[linkedSession.sessionId, jobId]] : [])];
|
|
136
153
|
}));
|
|
137
|
-
const jobStateById = new Map(group.map(({ jobId, job }) => [jobId, job.state]));
|
|
138
|
-
const parentTurnFinished = (jobId) => ["done", "completed", "failed", "error", "cancelled", "canceled", "stopped"].includes((jobStateById.get(jobId) ?? "").toLowerCase());
|
|
139
154
|
const eventJobId = (event) => event.jobId ?? jobIdBySession.get(event.sessionId);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
label: prior?.label !== prior?.id ? prior.label : label,
|
|
168
|
-
startedAt: Math.min(prior?.startedAt ?? event.ts, event.ts),
|
|
169
|
-
doneAt: isStop ? event.ts : isStart ? undefined : prior?.doneAt,
|
|
170
|
-
updatedAt: event.ts,
|
|
171
|
-
running: isStart,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
const workerByNativeKey = new Map(workers.map((worker) => [`${worker.jobId}:${worker.id}`, worker]));
|
|
175
|
-
for (const native of nativeEvidence.values()) {
|
|
176
|
-
const key = `${native.jobId}:${native.id}`;
|
|
177
|
-
const fanWorker = workerByNativeKey.get(key);
|
|
178
|
-
const turnFinished = parentTurnFinished(native.jobId);
|
|
179
|
-
// SubagentStop is the worker's own terminal lifecycle boundary. The
|
|
180
|
-
// manager may remain idle/open for follow-up, but that must not keep a
|
|
181
|
-
// finished worker seated indefinitely. A later SubagentStart for the
|
|
182
|
-
// same native id clears native.doneAt above and resumes that person.
|
|
183
|
-
const workerFinished = Boolean(native.doneAt) || turnFinished;
|
|
184
|
-
const merged = fanWorker ? {
|
|
185
|
-
...fanWorker,
|
|
186
|
-
label: fanWorker.label || native.label,
|
|
187
|
-
startedAt: Math.min(fanWorker.startedAt, native.startedAt),
|
|
188
|
-
doneAt: native.doneAt ?? (turnFinished ? fanWorker.doneAt : undefined),
|
|
189
|
-
updatedAt: Math.max(fanWorker.updatedAt ?? 0, native.updatedAt),
|
|
190
|
-
jobState: workerFinished ? "done" : native.running ? "working" : "idle",
|
|
191
|
-
} : {
|
|
192
|
-
id: native.id,
|
|
193
|
-
kind: "agent",
|
|
194
|
-
label: native.label,
|
|
195
|
-
startedAt: native.startedAt,
|
|
196
|
-
doneAt: native.doneAt,
|
|
197
|
-
updatedAt: native.updatedAt,
|
|
198
|
-
jobId: native.jobId,
|
|
199
|
-
jobState: workerFinished ? "done" : native.running ? "working" : "idle",
|
|
200
|
-
};
|
|
201
|
-
workerByNativeKey.set(key, merged);
|
|
155
|
+
const missionSummary = missions?.summaryFor(missionId, oldest.job.intent ?? "");
|
|
156
|
+
const turnCompleted = latest.job.state === "done";
|
|
157
|
+
// Every manager on this mission the provider can still act through. A
|
|
158
|
+
// mission is meant to have one; more than one means a resume forked
|
|
159
|
+
// instead of continuing, and the extras are still working the same
|
|
160
|
+
// worktree. Both the roster and the alerts need to know.
|
|
161
|
+
const liveJobIds = group.filter(({ jobId, job }) => jobIsLive(jobId, job, sessions.values())).map(({ jobId }) => jobId);
|
|
162
|
+
// One canonical person per delegated job. The resolver owns every identity
|
|
163
|
+
// decision — native ids, delegation labels, plan tasks, nested agents —
|
|
164
|
+
// and persists them so a later poll cannot re-decide them differently.
|
|
165
|
+
const resolved = resolveMissionWorkers({
|
|
166
|
+
missionId,
|
|
167
|
+
fanWorkers,
|
|
168
|
+
events: allEvents,
|
|
169
|
+
eventJobId,
|
|
170
|
+
planTasks: planStore?.get(missionId)?.phases.flatMap((phase) => allPlanTasks(phase.tasks)),
|
|
171
|
+
missionActive: (missionSummary?.lifecycleStatus ?? "active") === "active",
|
|
172
|
+
seeded: workerIdentity?.list(missionId),
|
|
173
|
+
seededNested: workerIdentity?.listNested(missionId),
|
|
174
|
+
now,
|
|
175
|
+
correlationGraceMs: workerCorrelationGraceMs,
|
|
176
|
+
liveJobIds,
|
|
177
|
+
});
|
|
178
|
+
let workers = resolved.workers;
|
|
179
|
+
if (workerIdentity && resolved.dirty) {
|
|
180
|
+
workerIdentity.save(missionId, resolved.records);
|
|
181
|
+
workerIdentity.saveNested(missionId, resolved.nested);
|
|
202
182
|
}
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
workersByIdentity.set(worker.id, worker);
|
|
213
|
-
continue;
|
|
183
|
+
// Keep the board honest without waiting for the orchestrator to republish
|
|
184
|
+
// it: a worker that is provably running advances its task to working.
|
|
185
|
+
if (planStore) {
|
|
186
|
+
// `doneAt` is the only lifecycle signal; `jobState` is decoration.
|
|
187
|
+
const present = workers.filter((worker) => !worker.doneAt);
|
|
188
|
+
for (const binding of reconcilePlanWorkers(planStore.get(missionId), present)) {
|
|
189
|
+
planStore.attachWorker(missionId, binding);
|
|
190
|
+
if (binding.inferred)
|
|
191
|
+
workerIdentity?.addTaskBinding(missionId, binding.canonicalWorker, binding.taskId);
|
|
214
192
|
}
|
|
215
|
-
const priorFreshness = prior.updatedAt ?? prior.doneAt ?? prior.startedAt;
|
|
216
|
-
const workerFreshness = worker.updatedAt ?? worker.doneAt ?? worker.startedAt;
|
|
217
|
-
const latest = workerFreshness >= priorFreshness ? worker : prior;
|
|
218
|
-
workersByIdentity.set(worker.id, {
|
|
219
|
-
...latest,
|
|
220
|
-
label: latest.label || prior.label,
|
|
221
|
-
startedAt: Math.min(prior.startedAt, worker.startedAt),
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
workers = [...workersByIdentity.values()];
|
|
225
|
-
// Friendly/custom worker names are presentation aliases only. Native
|
|
226
|
-
// Claude ids remain canonical so close or repeated delegations can never
|
|
227
|
-
// swap React identities or manufacture an extra avatar.
|
|
228
|
-
const workerAliases = new Map();
|
|
229
|
-
const claimedNativeWorkers = new Set();
|
|
230
|
-
for (const event of allEvents.filter((candidate) => candidate.source === "custom" && candidate.phase === "delegating" && candidate.targetWorker)) {
|
|
231
|
-
const resolvedJobId = eventJobId(event);
|
|
232
|
-
if (!resolvedJobId)
|
|
233
|
-
continue;
|
|
234
|
-
const aliasKey = `${resolvedJobId}:${event.targetWorker}`;
|
|
235
|
-
const candidate = workers
|
|
236
|
-
.filter((worker) => worker.jobId === resolvedJobId && !claimedNativeWorkers.has(worker.id) && Math.abs(worker.startedAt - event.ts) <= 30_000)
|
|
237
|
-
.sort((a, b) => Math.abs(a.startedAt - event.ts) - Math.abs(b.startedAt - event.ts))[0];
|
|
238
|
-
workerAliases.set(aliasKey, candidate?.id ?? aliasKey);
|
|
239
|
-
if (!candidate)
|
|
240
|
-
continue;
|
|
241
|
-
claimedNativeWorkers.add(candidate.id);
|
|
242
193
|
}
|
|
243
|
-
|
|
244
|
-
recentEvents = recentEvents.map((event) => {
|
|
245
|
-
if (!event.targetWorker)
|
|
246
|
-
return event;
|
|
247
|
-
const resolvedJobId = eventJobId(event);
|
|
248
|
-
if (!resolvedJobId)
|
|
249
|
-
return event;
|
|
250
|
-
const key = `${resolvedJobId}:${event.targetWorker}`;
|
|
251
|
-
const targetWorker = workerAliases.get(key);
|
|
252
|
-
return targetWorker ? { ...event, targetWorker } : event;
|
|
253
|
-
});
|
|
254
|
-
// Claude's fan file can arrive just before its delegation hook. Publishing
|
|
255
|
-
// that unmatched native id for a single poll makes the office animate a
|
|
256
|
-
// second person. Hold it briefly for correlation; canonical/event-backed
|
|
257
|
-
// workers remain immediate, and hookless workers appear after the grace.
|
|
258
|
-
const correlatedWorkerIds = new Set(recentEvents.filter((event) => event.source === "custom" && event.phase === "delegating").map((event) => event.targetWorker).filter((id) => Boolean(id)));
|
|
259
|
-
workers = workers.filter((worker) => nativeEvidence.has(`${worker.jobId}:${worker.id}`) || correlatedWorkerIds.has(worker.id) || now - worker.startedAt >= workerCorrelationGraceMs);
|
|
194
|
+
recentEvents = recentEvents.map(resolved.rewrite);
|
|
260
195
|
const derived = deriveStatus(session, latest.job, recentEvents, now);
|
|
261
|
-
const workerIds = new Set(workers.map((worker) => worker.id));
|
|
262
|
-
for (const event of recentEvents) {
|
|
263
|
-
if (!event.targetWorker || workerIds.has(event.targetWorker))
|
|
264
|
-
continue;
|
|
265
|
-
const related = recentEvents.filter((candidate) => candidate.targetWorker === event.targetWorker);
|
|
266
|
-
const start = related.find((candidate) => candidate.phase === "delegating" || candidate.phase?.toLowerCase().includes("subagentstart") || candidate.hookEventName?.toLowerCase() === "subagentstart");
|
|
267
|
-
if (!start)
|
|
268
|
-
continue;
|
|
269
|
-
// A friendly delegation target is an alias, not a person. Hold every
|
|
270
|
-
// unmatched identity through the correlation window; a native Agent id
|
|
271
|
-
// normally arrives within milliseconds and becomes the sole actor.
|
|
272
|
-
if (now - start.ts < workerCorrelationGraceMs)
|
|
273
|
-
continue;
|
|
274
|
-
const done = [...related].reverse().find((candidate) => candidate.phase === "worker_reported" || candidate.phase?.toLowerCase().includes("subagentstop") || candidate.hookEventName?.toLowerCase() === "subagentstop");
|
|
275
|
-
const resolvedJobId = eventJobId(event);
|
|
276
|
-
if (!resolvedJobId)
|
|
277
|
-
continue;
|
|
278
|
-
workers.push({ id: event.targetWorker, label: event.targetWorker, kind: "subagent", startedAt: start.ts, doneAt: done?.ts, updatedAt: related.at(-1)?.ts, jobId: resolvedJobId, jobState: done ? "done" : latest.job.state });
|
|
279
|
-
workerIds.add(event.targetWorker);
|
|
280
|
-
}
|
|
281
196
|
const derivedMessages = group.flatMap(({ jobId, job }) => {
|
|
282
197
|
const createdAt = dateMs(job.createdAt);
|
|
283
198
|
const updatedAt = dateMs(job.updatedAt) || createdAt;
|
|
@@ -319,7 +234,9 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
319
234
|
lifecycleStatus: "active",
|
|
320
235
|
mode: "orchestrated",
|
|
321
236
|
};
|
|
322
|
-
|
|
237
|
+
// Derived at read time rather than written back: the events are the source
|
|
238
|
+
// of truth, so this always reflects the latest ones without extra writes.
|
|
239
|
+
const missionPlan = deriveChangedFiles(planStore?.get(missionId), workers, recentEvents);
|
|
323
240
|
const inactiveForMs = Math.max(0, now - updatedAt);
|
|
324
241
|
let activityStatus = derived.status;
|
|
325
242
|
if (mission.lifecycleStatus === "completed" || mission.lifecycleStatus === "archived") {
|
|
@@ -338,20 +255,34 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
338
255
|
// In that case the runtime is idle, not mission-complete.
|
|
339
256
|
if (activityStatus === "done")
|
|
340
257
|
activityStatus = "idle";
|
|
341
|
-
|
|
342
|
-
|
|
258
|
+
// Provably running right now, as opposed to a plan that merely claims
|
|
259
|
+
// work is open. Workers outlive the manager's own session going quiet
|
|
260
|
+
// between turns, so a mission with live workers is working — reading
|
|
261
|
+
// "Idle" while four of them report progress is simply wrong.
|
|
262
|
+
const liveWork = session?.status === "busy"
|
|
263
|
+
|| latest.job.state === "working" || latest.job.state === "busy"
|
|
264
|
+
|| workers.some((worker) => !worker.doneAt && (worker.jobState === "working" || worker.jobState === "busy"));
|
|
265
|
+
const workExpected = liveWork || Boolean(missionPlan?.phases.some((phase) => phase.status === "working" || phase.status === "reviewing" || allPlanTasks(phase.tasks).some((task) => task.status === "working" || task.status === "reviewing")));
|
|
266
|
+
if ((activityStatus === "idle" || activityStatus === "working") && workExpected && inactiveForMs >= config.stalledAfterMs) {
|
|
267
|
+
// Covers "working" too: a job record left behind by a dead process
|
|
268
|
+
// still claims to be working, and only the elapsed time known here can
|
|
269
|
+
// tell that apart from work actually in progress.
|
|
343
270
|
activityStatus = "stalled";
|
|
344
271
|
}
|
|
272
|
+
else if (activityStatus === "idle" && liveWork) {
|
|
273
|
+
// A stale plan can claim work forever, so only provably live work
|
|
274
|
+
// promotes an idle mission; a plan's claim alone must not.
|
|
275
|
+
activityStatus = "working";
|
|
276
|
+
}
|
|
345
277
|
}
|
|
346
278
|
const name = missions?.nameFor(missionId) ?? oldest.job.name ?? latest.job.name ?? missionId;
|
|
347
|
-
const turnCompleted = latest.job.state === "done";
|
|
348
279
|
const pendingDecisions = pendingDecisionsFor({ missionId, name, jobId: latest.jobId, mission, lifecycleStatus: mission.lifecycleStatus, turnCompleted, recentEvents, updatedAt });
|
|
349
280
|
// An unanswered decision is, by definition, waiting on the user. Do not let the
|
|
350
281
|
// runtime's quiet session decay it to idle/stalled while the question stands.
|
|
351
282
|
if (mission.lifecycleStatus === "active" && pendingDecisions.length && (activityStatus === "idle" || activityStatus === "stalled"))
|
|
352
283
|
activityStatus = "waiting_on_you";
|
|
353
284
|
const runStartedAt = dateMs(latest.job.createdAt) || createdAt;
|
|
354
|
-
const alerts = deriveAlerts({ lifecycle: mission.lifecycleStatus, activity: activityStatus, stale: derived.stale, turnCompleted, workers, events: recentEvents, now, turnTokens: latest.job.tokens ?? 0, runStartedAt });
|
|
285
|
+
const alerts = deriveAlerts({ lifecycle: mission.lifecycleStatus, activity: activityStatus, stale: derived.stale, turnCompleted, workers, events: recentEvents, now, turnTokens: latest.job.tokens ?? 0, runStartedAt, liveJobIds });
|
|
355
286
|
orchestrators.push({
|
|
356
287
|
missionId,
|
|
357
288
|
threadId: missionId,
|