@shanesaravia/hive 0.4.0 → 0.5.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 +23 -0
- package/node_modules/@hive/shared/dist/types.d.ts +8 -0
- package/node_modules/@hive/shared/dist/types.js +8 -0
- package/package.json +1 -1
- package/packages/server/dist/api/rest.js +109 -20
- package/packages/server/dist/control/missionQuiesce.js +4 -0
- package/packages/server/dist/loops/loopCommand.js +20 -0
- package/packages/server/dist/messages/messagesStore.js +28 -4
- package/packages/server/dist/missions/missionsStore.js +1 -1
- package/packages/server/dist/missions/reopenOnWork.js +7 -0
- package/packages/server/dist/paths/pathResolver.js +112 -17
- package/packages/server/dist/plans/plansStore.js +32 -2
- package/packages/server/dist/roster/workerIdentity.js +91 -23
- package/packages/web/dist/assets/index-C1T22qkK.css +2 -0
- package/packages/web/dist/assets/index-CVc04VBT.js +18 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/index-DWjqiitn.js +0 -17
- package/packages/web/dist/assets/index-rd4RnLqj.css +0 -2
|
@@ -72,6 +72,33 @@ function normalizeGates(gates) {
|
|
|
72
72
|
return { ...gate, id, label: gate.label.trim(), required: gate.required !== false, evidence: gate.evidence ?? [], waiver: gate.waiver ? { ...gate.waiver, reason: gate.waiver.reason.trim() } : undefined };
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
+
function gateKey(gate) {
|
|
76
|
+
return `${gate.type}:${gate.label.trim().toLocaleLowerCase().replace(/\s+/g, " ")}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* User-created gates are durable requirements, but the orchestrator is the
|
|
80
|
+
* actor that supplies evidence and reports when the requirement is satisfied.
|
|
81
|
+
* Reconcile an equivalent reported gate onto the protected user gate instead
|
|
82
|
+
* of retaining both IDs. The orchestrator may satisfy or fail the requirement,
|
|
83
|
+
* but only the user may waive it or weaken its definition.
|
|
84
|
+
*/
|
|
85
|
+
function reconcileUserGates(gates) {
|
|
86
|
+
const userGates = gates.filter((gate) => gate.source === "user");
|
|
87
|
+
if (!userGates.length)
|
|
88
|
+
return gates;
|
|
89
|
+
const reported = gates.filter((gate) => gate.source !== "user");
|
|
90
|
+
const matches = (userGate) => reported.filter((gate) => gate.id === userGate.id || gateKey(gate) === gateKey(userGate));
|
|
91
|
+
const matchedIds = new Set(userGates.flatMap((gate) => matches(gate).map((item) => item.id)));
|
|
92
|
+
const reconciled = userGates.map((gate) => {
|
|
93
|
+
const equivalents = matches(gate);
|
|
94
|
+
const satisfied = equivalents.find((item) => item.status === "satisfied");
|
|
95
|
+
const failed = equivalents.find((item) => item.status === "failed");
|
|
96
|
+
const status = gate.status === "satisfied" || gate.status === "waived" ? gate.status : satisfied ? "satisfied" : failed ? "failed" : gate.status;
|
|
97
|
+
const evidence = [...new Set([...(gate.evidence ?? []), ...equivalents.flatMap((item) => item.evidence ?? [])])];
|
|
98
|
+
return { ...gate, status, evidence };
|
|
99
|
+
});
|
|
100
|
+
return [...reported.filter((gate) => !matchedIds.has(gate.id)), ...reconciled];
|
|
101
|
+
}
|
|
75
102
|
/** Durable current plan plus immutable JSON snapshots for revision history. */
|
|
76
103
|
export class PlansStore {
|
|
77
104
|
db;
|
|
@@ -91,7 +118,7 @@ export class PlansStore {
|
|
|
91
118
|
const incoming = input.gates === undefined ? current?.gates ?? [] : normalizeGates(input.gates);
|
|
92
119
|
const protectedUserGates = preserveUserGates ? (current?.gates ?? []).filter((gate) => gate.source === "user") : [];
|
|
93
120
|
const protectedIds = new Set(protectedUserGates.map((gate) => gate.id));
|
|
94
|
-
const gates = [...incoming.filter((gate) => !protectedIds.has(gate.id)), ...protectedUserGates];
|
|
121
|
+
const gates = reconcileUserGates([...incoming.filter((gate) => !protectedIds.has(gate.id)), ...protectedUserGates]);
|
|
95
122
|
const plan = { missionId, revision: (current?.revision ?? 0) + 1, updatedAt: Date.now(), phases, gates, layout: input.tasks ? "flat" : "phased", approvalStatus: input.approvalStatus ?? "proposed", approvalReason: input.approvalReason?.trim() || undefined, progress: calculateProgress(phases) };
|
|
96
123
|
const json = JSON.stringify(plan);
|
|
97
124
|
this.db.transaction(() => {
|
|
@@ -209,7 +236,10 @@ export class PlansStore {
|
|
|
209
236
|
this.db.prepare("INSERT OR REPLACE INTO mission_plan_revisions (mission_id, revision, updated_at, plan_json) VALUES (?, ?, ?, ?)").run(newId, revision.revision, revision.updatedAt, JSON.stringify({ ...revision, missionId: newId })); this.db.prepare("DELETE FROM mission_plans WHERE mission_id = ?").run(oldId); this.db.prepare("DELETE FROM mission_plan_revisions WHERE mission_id = ?").run(oldId); })(); }
|
|
210
237
|
get(missionId) {
|
|
211
238
|
const row = this.db.prepare("SELECT plan_json FROM mission_plans WHERE mission_id = ?").get(missionId);
|
|
212
|
-
|
|
239
|
+
if (!row)
|
|
240
|
+
return undefined;
|
|
241
|
+
const plan = JSON.parse(row.plan_json);
|
|
242
|
+
return { ...plan, gates: reconcileUserGates(plan.gates ?? []) };
|
|
213
243
|
}
|
|
214
244
|
revisions(missionId) {
|
|
215
245
|
return this.db.prepare("SELECT plan_json FROM mission_plan_revisions WHERE mission_id = ? ORDER BY revision DESC").all(missionId).map((row) => JSON.parse(row.plan_json));
|
|
@@ -131,13 +131,62 @@ const LAUNCH_NAME_GRACE_MS = 3_000;
|
|
|
131
131
|
*/
|
|
132
132
|
export class WorkerIdentityTable {
|
|
133
133
|
missionId;
|
|
134
|
+
liveJobIds;
|
|
134
135
|
byCanonical = new Map();
|
|
135
136
|
dirty = false;
|
|
136
|
-
constructor(missionId, seeded = []) {
|
|
137
|
+
constructor(missionId, seeded = [], liveJobIds = new Set()) {
|
|
137
138
|
this.missionId = missionId;
|
|
139
|
+
this.liveJobIds = liveJobIds;
|
|
138
140
|
for (const record of seeded)
|
|
139
141
|
this.byCanonical.set(record.canonicalId, { ...record, nativeIds: [...record.nativeIds], labels: [...record.labels], planTaskIds: [...record.planTaskIds], sessionIds: [...record.sessionIds] });
|
|
140
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Whether this record belongs to a manager other than the one asking, and
|
|
145
|
+
* so must not be found by name.
|
|
146
|
+
*
|
|
147
|
+
* Only a name can be ambiguous. A provider-native agent id is unique by
|
|
148
|
+
* construction and means the same worker wherever it turns up. A plan task
|
|
149
|
+
* id and a delegation label are words an orchestrator chose, and two
|
|
150
|
+
* managers live on one mission choose the same words for different people:
|
|
151
|
+
* on mission f7ae71c7 two of them delegated plan task `t12` to a worker they
|
|
152
|
+
* both called `mr-babysitter`, three minutes apart. Matched across the two,
|
|
153
|
+
* one manager's report retired the other manager's worker.
|
|
154
|
+
*
|
|
155
|
+
* Two conditions keep this from splitting workers that ought to merge. The
|
|
156
|
+
* other job must still be able to act — a job that is over is not competing
|
|
157
|
+
* for anything, so a task picked up on a later turn still finds the worker
|
|
158
|
+
* whose task it is. And the worker must still be running: one that has
|
|
159
|
+
* finished is nobody's to guard, and adopting its task is exactly the retry
|
|
160
|
+
* this table exists to recognise.
|
|
161
|
+
*/
|
|
162
|
+
ownedElsewhere(record, jobId) {
|
|
163
|
+
if (!jobId || !record.lastJobId || record.lastJobId === jobId)
|
|
164
|
+
return false;
|
|
165
|
+
return this.liveJobIds.has(record.lastJobId) && !record.doneAt;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A canonical id no other record has taken.
|
|
169
|
+
*
|
|
170
|
+
* A native id is unique already. A label is not, and two live managers can
|
|
171
|
+
* hand the same one to two different workers — where the table used to merge
|
|
172
|
+
* them it now keeps them apart, and keying both on the label would silently
|
|
173
|
+
* drop the first from the floor. The label stays the worker's display name
|
|
174
|
+
* either way; only the key it is filed under gains the owning job.
|
|
175
|
+
*/
|
|
176
|
+
freeCanonicalId(label, jobId) {
|
|
177
|
+
if (!this.byCanonical.has(label))
|
|
178
|
+
return label;
|
|
179
|
+
const scoped = `${label}#${(jobId ?? "").slice(0, 8)}`;
|
|
180
|
+
if (jobId && !this.byCanonical.has(scoped))
|
|
181
|
+
return scoped;
|
|
182
|
+
for (let suffix = 2; suffix <= this.byCanonical.size + 2; suffix += 1) {
|
|
183
|
+
const candidate = `${jobId ? scoped : label}#${suffix}`;
|
|
184
|
+
if (!this.byCanonical.has(candidate))
|
|
185
|
+
return candidate;
|
|
186
|
+
}
|
|
187
|
+
// Unreachable: more candidates are tried above than there are records.
|
|
188
|
+
return `${label}#${this.byCanonical.size + 3}`;
|
|
189
|
+
}
|
|
141
190
|
/** Flags a change made by a later pass, so the record set is persisted. */
|
|
142
191
|
markDirty() { this.dirty = true; }
|
|
143
192
|
/**
|
|
@@ -165,15 +214,17 @@ export class WorkerIdentityTable {
|
|
|
165
214
|
if (match)
|
|
166
215
|
return match;
|
|
167
216
|
}
|
|
217
|
+
// Names, unlike native ids, can belong to a live sibling manager.
|
|
218
|
+
const mine = (record) => !this.ownedElsewhere(record, ref.jobId);
|
|
168
219
|
const task = norm(ref.taskId);
|
|
169
220
|
if (task) {
|
|
170
|
-
const match = this.records().find((record) => record.planTaskIds.some((id) => id.toLowerCase() === task));
|
|
221
|
+
const match = this.records().find((record) => mine(record) && record.planTaskIds.some((id) => id.toLowerCase() === task));
|
|
171
222
|
if (match)
|
|
172
223
|
return match;
|
|
173
224
|
}
|
|
174
225
|
const label = norm(ref.label);
|
|
175
226
|
if (label) {
|
|
176
|
-
const match = this.records().find((record) => record.labels.some((item) => item.toLowerCase() === label));
|
|
227
|
+
const match = this.records().find((record) => mine(record) && record.labels.some((item) => item.toLowerCase() === label));
|
|
177
228
|
if (match)
|
|
178
229
|
return match;
|
|
179
230
|
}
|
|
@@ -185,13 +236,16 @@ export class WorkerIdentityTable {
|
|
|
185
236
|
}
|
|
186
237
|
/** Merge this evidence into an existing person, or create one. */
|
|
187
238
|
bind(ref, into, options = {}) {
|
|
188
|
-
const existing = into ?? (options.allowLabelMerge === false ? this.find({ nativeId: ref.nativeId, taskId: ref.taskId }) : this.find(ref));
|
|
239
|
+
const existing = into ?? (options.allowLabelMerge === false ? this.find({ nativeId: ref.nativeId, taskId: ref.taskId, jobId: ref.jobId }) : this.find(ref));
|
|
189
240
|
if (!existing) {
|
|
190
241
|
// A native id is the stable, provider-owned name and is preferred as the
|
|
191
242
|
// canonical id. A label-only worker (Codex, or a delegation whose launch
|
|
192
243
|
// hook never arrived) is keyed by its label, scoped to this mission
|
|
193
|
-
// rather than to a single parent job so a resumed turn reuses it
|
|
194
|
-
|
|
244
|
+
// rather than to a single parent job so a resumed turn reuses it — and
|
|
245
|
+
// disambiguated by job only where a live sibling already holds the name.
|
|
246
|
+
const native = ref.nativeId?.trim();
|
|
247
|
+
const label = ref.label?.trim();
|
|
248
|
+
const canonicalId = native || (label ? this.freeCanonicalId(label, ref.jobId) : undefined);
|
|
195
249
|
if (!canonicalId)
|
|
196
250
|
throw new Error("worker identity requires a native id or a label");
|
|
197
251
|
const record = { canonicalId, missionId: this.missionId, nativeIds: [], labels: [], planTaskIds: [], sessionIds: [], startedAt: ref.at, lastJobId: ref.jobId };
|
|
@@ -273,14 +327,26 @@ export class WorkerIdentityTable {
|
|
|
273
327
|
this.dirty = true;
|
|
274
328
|
return existing;
|
|
275
329
|
}
|
|
276
|
-
/**
|
|
277
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Canonical id for any alias, or undefined when nothing has claimed it.
|
|
332
|
+
*
|
|
333
|
+
* `jobId` is the manager speaking. Where two of them are live and have used
|
|
334
|
+
* the same word for two different workers, the speaker's own worker is the
|
|
335
|
+
* one it means — without that, a manager reporting its worker finished
|
|
336
|
+
* retired whichever record happened to be found first, and the wrong avatar
|
|
337
|
+
* left the floor.
|
|
338
|
+
*/
|
|
339
|
+
canonicalFor(value, jobId) {
|
|
278
340
|
if (!value)
|
|
279
341
|
return undefined;
|
|
280
342
|
const wanted = norm(value);
|
|
281
343
|
if (!wanted)
|
|
282
344
|
return undefined;
|
|
283
|
-
|
|
345
|
+
const matches = this.records().filter((record) => [record.canonicalId, ...record.nativeIds, ...record.labels, ...record.planTaskIds].some((item) => item.toLowerCase() === wanted));
|
|
346
|
+
if (!matches.length)
|
|
347
|
+
return undefined;
|
|
348
|
+
const own = jobId ? matches.find((record) => record.lastJobId === jobId) : undefined;
|
|
349
|
+
return (own ?? matches.find((record) => !this.ownedElsewhere(record, jobId)) ?? matches[0]).canonicalId;
|
|
284
350
|
}
|
|
285
351
|
}
|
|
286
352
|
/**
|
|
@@ -291,9 +357,10 @@ export class WorkerIdentityTable {
|
|
|
291
357
|
* label stranded as a third. Version 5 stopped a launch under a still-live
|
|
292
358
|
* sibling job being read as a replacement, which merged the workers of
|
|
293
359
|
* concurrent managers into one person — and clears the records those bindings
|
|
294
|
-
* already wrote.
|
|
360
|
+
* already wrote. Version 6 stopped a plan task or delegation label finding a
|
|
361
|
+
* live sibling manager's worker at all.
|
|
295
362
|
*/
|
|
296
|
-
export const WORKER_IDENTITY_RESOLVER_VERSION = "
|
|
363
|
+
export const WORKER_IDENTITY_RESOLVER_VERSION = "6";
|
|
297
364
|
/** Persists resolved identities so they survive the bounded event ring. */
|
|
298
365
|
export class WorkerIdentityStore {
|
|
299
366
|
db;
|
|
@@ -361,12 +428,13 @@ export class WorkerIdentityStore {
|
|
|
361
428
|
.run(JSON.stringify(record), Date.now(), missionId, canonicalId);
|
|
362
429
|
}
|
|
363
430
|
/** Canonical worker id for any alias, label, or plan task id. */
|
|
364
|
-
canonicalFor(missionId, value) {
|
|
431
|
+
canonicalFor(missionId, value, jobId) {
|
|
365
432
|
const wanted = value?.trim().toLowerCase();
|
|
366
433
|
if (!wanted)
|
|
367
434
|
return undefined;
|
|
368
|
-
|
|
369
|
-
.some((item) => item.trim().toLowerCase() === wanted))
|
|
435
|
+
const matches = this.list(missionId).filter((record) => [record.canonicalId, ...record.nativeIds, ...record.labels, ...record.planTaskIds]
|
|
436
|
+
.some((item) => item.trim().toLowerCase() === wanted));
|
|
437
|
+
return (jobId ? matches.find((record) => record.lastJobId === jobId) : undefined)?.canonicalId ?? matches[0]?.canonicalId;
|
|
370
438
|
}
|
|
371
439
|
removeMission(missionId) {
|
|
372
440
|
this.db.prepare("DELETE FROM mission_workers WHERE mission_id = ?").run(missionId);
|
|
@@ -405,7 +473,7 @@ export function resolveMissionWorkers(input) {
|
|
|
405
473
|
const pairWindowMs = input.pairWindowMs ?? 30_000;
|
|
406
474
|
const liveJobIds = new Set(input.liveJobIds ?? []);
|
|
407
475
|
const planTasksInput = input.planTasks ?? [];
|
|
408
|
-
const table = new WorkerIdentityTable(missionId, input.seeded);
|
|
476
|
+
const table = new WorkerIdentityTable(missionId, input.seeded, liveJobIds);
|
|
409
477
|
// 1. Hook lifecycle evidence, keyed by the provider's own agent id.
|
|
410
478
|
const nativeEvidence = new Map();
|
|
411
479
|
// An agent launched by a worker is that worker's activity, not a new person
|
|
@@ -545,7 +613,7 @@ export function resolveMissionWorkers(input) {
|
|
|
545
613
|
// worker there is nothing a name could change, so it binds immediately as it
|
|
546
614
|
// always has; it is two delegations reaching for the same anonymous launch
|
|
547
615
|
// that need the description to arrive first.
|
|
548
|
-
const contenders = delegations.filter((event) => !table.find({ label: event.targetWorker?.trim() || undefined, taskId: identityTask(event) })?.nativeIds.length).length;
|
|
616
|
+
const contenders = delegations.filter((event) => !table.find({ label: event.targetWorker?.trim() || undefined, taskId: identityTask(event), jobId: eventJobId(event) })?.nativeIds.length).length;
|
|
549
617
|
// Decide nothing while a name may still be arriving. Deferring the whole
|
|
550
618
|
// pass rather than the individual launch matters: pairing sees every launch
|
|
551
619
|
// at once either way, so waiting cannot change which delegation wins a
|
|
@@ -553,7 +621,7 @@ export function resolveMissionWorkers(input) {
|
|
|
553
621
|
const waitingForNames = contenders > 1
|
|
554
622
|
&& launchOrder.some((launch) => !launchIsNamed(launch.nativeId) && now - launch.startedAt < LAUNCH_NAME_GRACE_MS);
|
|
555
623
|
const namedLaunches = waitingForNames ? [] : launchOrder;
|
|
556
|
-
const needsLaunch = (event) => !table.find({ label: event.targetWorker?.trim() || undefined, taskId: identityTask(event) })?.nativeIds.length;
|
|
624
|
+
const needsLaunch = (event) => !table.find({ label: event.targetWorker?.trim() || undefined, taskId: identityTask(event), jobId: eventJobId(event) })?.nativeIds.length;
|
|
557
625
|
const pairing = pairDelegationsByName({
|
|
558
626
|
delegations: delegations.filter(needsLaunch),
|
|
559
627
|
launches: namedLaunches.filter((launch) => !claimed.has(launch.nativeId)),
|
|
@@ -583,7 +651,7 @@ export function resolveMissionWorkers(input) {
|
|
|
583
651
|
return false;
|
|
584
652
|
if (pairing.has(other))
|
|
585
653
|
return false;
|
|
586
|
-
const otherNamed = table.find({ label: other.targetWorker?.trim() || undefined, taskId: identityTask(other) });
|
|
654
|
+
const otherNamed = table.find({ label: other.targetWorker?.trim() || undefined, taskId: identityTask(other), jobId });
|
|
587
655
|
if (otherNamed?.nativeIds.length)
|
|
588
656
|
return false;
|
|
589
657
|
return Math.abs(startedAt - other.ts) < Math.abs(startedAt - event.ts);
|
|
@@ -601,7 +669,7 @@ export function resolveMissionWorkers(input) {
|
|
|
601
669
|
// An already-known label or plan task wins over the launch: a retried or
|
|
602
670
|
// replaced task keeps its person, and the fresh provider id joins it as
|
|
603
671
|
// another alias instead of taking a second desk.
|
|
604
|
-
const named = table.find({ label, taskId: identityTask(event) });
|
|
672
|
+
const named = table.find({ label, taskId: identityTask(event), jobId });
|
|
605
673
|
const known = named ?? (pairable ? table.find({ nativeId: pairable.nativeId }) : undefined);
|
|
606
674
|
// A delegation that already has its worker must not swallow a later,
|
|
607
675
|
// unrelated launch just because it is still inside the pairing window —
|
|
@@ -667,7 +735,7 @@ export function resolveMissionWorkers(input) {
|
|
|
667
735
|
// still running. Merging by description exists for a retry — a fresh id for
|
|
668
736
|
// work whose previous attempt has stopped or been reported — and
|
|
669
737
|
// orchestrators reuse descriptions for parallel workers.
|
|
670
|
-
const sameName = label ? table.find({ label }) : undefined;
|
|
738
|
+
const sameName = label ? table.find({ label, jobId: native.jobId }) : undefined;
|
|
671
739
|
const sameNameLive = Boolean(sameName?.nativeIds.length) && sameName.nativeIds.some((id) => { const evidence = nativeEvidence.get(id); return evidence ? !evidence.doneAt : false; })
|
|
672
740
|
&& ![sameName.canonicalId, ...sameName.nativeIds, ...sameName.labels, ...sameName.planTaskIds].some((value) => reportedRefs.has(norm(value) ?? ""));
|
|
673
741
|
const ref = { nativeId: native.nativeId, label, jobId: native.jobId, sessionId: native.sessionId, at: native.startedAt };
|
|
@@ -691,7 +759,7 @@ export function resolveMissionWorkers(input) {
|
|
|
691
759
|
// it, and nobody when the manager did.
|
|
692
760
|
const canonical = event.toolName === "Agent"
|
|
693
761
|
? (event.workerRef?.parentAgentId ? table.canonicalFor(event.workerRef.parentAgentId) : undefined)
|
|
694
|
-
: table.canonicalFor(event.workerRef?.agentId) ?? table.canonicalFor(event.targetWorker);
|
|
762
|
+
: table.canonicalFor(event.workerRef?.agentId) ?? table.canonicalFor(event.targetWorker, eventJobId(event));
|
|
695
763
|
if (canonical)
|
|
696
764
|
backgroundedAt.set(canonical, Math.max(backgroundedAt.get(canonical) ?? 0, event.ts));
|
|
697
765
|
}
|
|
@@ -703,7 +771,7 @@ export function resolveMissionWorkers(input) {
|
|
|
703
771
|
for (const event of events) {
|
|
704
772
|
if (event.source !== "custom")
|
|
705
773
|
continue;
|
|
706
|
-
let canonical = table.canonicalFor(event.targetWorker) ?? table.canonicalFor(event.targetTask);
|
|
774
|
+
let canonical = table.canonicalFor(event.targetWorker, eventJobId(event)) ?? table.canonicalFor(event.targetTask, eventJobId(event));
|
|
707
775
|
if (!canonical)
|
|
708
776
|
continue;
|
|
709
777
|
// A delegation whose launch was never paired sits as an unpublished label
|
|
@@ -861,7 +929,7 @@ export function resolveMissionWorkers(input) {
|
|
|
861
929
|
// native id is the one that survives.
|
|
862
930
|
const delegationWaiting = !record.published && !record.delegated && record.nativeIds.length > 0
|
|
863
931
|
&& delegations.some((other) => eventJobId(other) === jobId && needsLaunch(other)
|
|
864
|
-
&& table.find({ label: other.targetWorker?.trim() || undefined, taskId: identityTask(other) })?.published)
|
|
932
|
+
&& table.find({ label: other.targetWorker?.trim() || undefined, taskId: identityTask(other), jobId: eventJobId(other) })?.published)
|
|
865
933
|
&& now - startedAt < correlationGraceMs;
|
|
866
934
|
if (mayYetPair || delegationWaiting || (!corroborated && now - startedAt < correlationGraceMs))
|
|
867
935
|
continue;
|