tryharness 0.7.22 → 0.7.25
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/README.md +48 -8
- package/assets/opencode-plugin.js +48 -10
- package/assets/pi-extension.ts +106 -26
- package/cli.mjs +1281 -289
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +604 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +177 -39
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +628 -0
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +281 -56
- package/lib/daemon.mjs +5590 -838
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +81 -0
- package/lib/environment.mjs +180 -0
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +288 -0
- package/lib/folders.mjs +223 -57
- package/lib/frames.mjs +333 -0
- package/lib/inference.mjs +663 -0
- package/lib/learn.mjs +182 -0
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +571 -0
- package/lib/mcp-stdio.mjs +912 -103
- package/lib/media.mjs +228 -0
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +190 -32
- package/lib/ocr.mjs +380 -0
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +77 -0
- package/lib/phones.mjs +398 -51
- package/lib/places.mjs +566 -102
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +82 -0
- package/lib/redact.mjs +730 -0
- package/lib/runner.mjs +347 -55
- package/lib/runtime-install.mjs +59 -17
- package/lib/screens.mjs +212 -0
- package/lib/sealed.mjs +160 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +173 -35
- package/lib/snippets.mjs +140 -0
- package/lib/speech.mjs +993 -0
- package/lib/store.mjs +1678 -301
- package/lib/swarm.mjs +598 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +401 -0
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +362 -0
- package/lib/worktrees.mjs +186 -38
- package/package.json +6 -4
- package/lib/embedder.mjs +0 -156
package/lib/coordinator.mjs
CHANGED
|
@@ -11,14 +11,19 @@ import { MACHINE_RUNNERS, MEMBERS, nameOf } from "./members.mjs";
|
|
|
11
11
|
|
|
12
12
|
const CODING = new Set(MACHINE_RUNNERS);
|
|
13
13
|
// The agents with a read-only switch (runner.mjs, review in the roster): only they can be handed a review.
|
|
14
|
-
const REVIEWERS = new Set(
|
|
14
|
+
const REVIEWERS = new Set(
|
|
15
|
+
MEMBERS.filter((entry) => entry.review).map((entry) => entry.id)
|
|
16
|
+
);
|
|
15
17
|
const TRANSCRIPT_POSTS = 30;
|
|
16
18
|
const QUIET_KINDS = new Set(["note", "held", "round"]);
|
|
17
19
|
// The coordinator's own posts are Harness's; Harness's answer to work it was given is a result like any agent's.
|
|
18
|
-
const ownPost = (thread, post) =>
|
|
20
|
+
const ownPost = (thread, post) =>
|
|
21
|
+
post.author === thread.goal.facilitator && post.kind !== "result";
|
|
19
22
|
// Harness's part with no Harness window open waits on the board for one (daemon.mjs offerToServer); the thread goes on.
|
|
20
|
-
const waitsForWindow = (run) =>
|
|
21
|
-
|
|
23
|
+
const waitsForWindow = (run) =>
|
|
24
|
+
run.status === "open" && run.target_agent === "harness";
|
|
25
|
+
const going = (run) =>
|
|
26
|
+
["open", "claimed", "running"].includes(run.status) && !waitsForWindow(run);
|
|
22
27
|
|
|
23
28
|
// The privacy setting: the round's facts go to an agent on this machine with its tools off, which answers in the
|
|
24
29
|
// same shape Harness does. Nothing reaches Harness's server.
|
|
@@ -32,69 +37,199 @@ If the same attempt or failure keeps coming back, say so in loop and ask the nex
|
|
|
32
37
|
function localDecision(agent, facts) {
|
|
33
38
|
return new Promise((resolve, reject) => {
|
|
34
39
|
const prompt = `${LOCAL_PROMPT}\n\n${JSON.stringify(facts)}`;
|
|
35
|
-
const invocation =
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const invocation =
|
|
41
|
+
agent === "codex"
|
|
42
|
+
? {
|
|
43
|
+
command: "codex",
|
|
44
|
+
args: [
|
|
45
|
+
"exec",
|
|
46
|
+
"--json",
|
|
47
|
+
"--sandbox",
|
|
48
|
+
"read-only",
|
|
49
|
+
"--skip-git-repo-check",
|
|
50
|
+
"-",
|
|
51
|
+
],
|
|
52
|
+
stdin: prompt,
|
|
53
|
+
}
|
|
54
|
+
: {
|
|
55
|
+
command: "claude",
|
|
56
|
+
args: ["-p", prompt, "--output-format", "json", "--tools", ""],
|
|
57
|
+
};
|
|
38
58
|
let child;
|
|
39
|
-
try {
|
|
59
|
+
try {
|
|
60
|
+
child = spawn(invocation.command, invocation.args, { cwd: os.tmpdir() });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
reject(error);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// An agent that exits before reading the prompt breaks the pipe; its exit code rejects below.
|
|
66
|
+
child.stdin.on("error", () => {});
|
|
40
67
|
child.stdin.end(invocation.stdin ?? "");
|
|
41
68
|
let out = "";
|
|
42
|
-
child.stdout.on("data", (chunk) => {
|
|
69
|
+
child.stdout.on("data", (chunk) => {
|
|
70
|
+
out += chunk;
|
|
71
|
+
});
|
|
43
72
|
child.on("error", reject);
|
|
44
73
|
child.on("close", (code) => {
|
|
45
|
-
if (code !== 0) {
|
|
74
|
+
if (code !== 0) {
|
|
75
|
+
reject(new Error(`${agent} exited with code ${code}`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
46
78
|
// Claude answers one JSON object with its text in `result`; Codex streams events, its words in agent_message.
|
|
47
79
|
let text = "";
|
|
48
80
|
for (const line of out.split("\n")) {
|
|
49
81
|
try {
|
|
50
82
|
const event = JSON.parse(line);
|
|
51
83
|
if (typeof event.result === "string") text = event.result;
|
|
52
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
event.type === "item.completed" &&
|
|
86
|
+
event.item?.type === "agent_message"
|
|
87
|
+
)
|
|
88
|
+
text = event.item.text;
|
|
53
89
|
} catch {}
|
|
54
90
|
}
|
|
55
91
|
const match = text.match(/\{[\s\S]*\}/);
|
|
56
|
-
try {
|
|
92
|
+
try {
|
|
93
|
+
resolve(JSON.parse(match ? match[0] : text));
|
|
94
|
+
} catch {
|
|
95
|
+
reject(new Error(`${agent} did not answer in the agreed shape`));
|
|
96
|
+
}
|
|
57
97
|
});
|
|
58
98
|
});
|
|
59
99
|
}
|
|
60
100
|
|
|
61
|
-
export function createCoordinator({
|
|
101
|
+
export function createCoordinator({
|
|
102
|
+
store,
|
|
103
|
+
mode,
|
|
104
|
+
askHarness,
|
|
105
|
+
connected,
|
|
106
|
+
serverHas,
|
|
107
|
+
roster,
|
|
108
|
+
handOn,
|
|
109
|
+
localAgents,
|
|
110
|
+
pauseGoal,
|
|
111
|
+
notifyBoard,
|
|
112
|
+
searchContext,
|
|
113
|
+
log,
|
|
114
|
+
frame = () => null,
|
|
115
|
+
}) {
|
|
62
116
|
const busy = new Set();
|
|
63
117
|
const again = new Set();
|
|
64
|
-
const stateOf = (goalId) =>
|
|
65
|
-
|
|
118
|
+
const stateOf = (goalId) =>
|
|
119
|
+
JSON.parse(
|
|
120
|
+
store.meta(`coordinator:${goalId}`) ||
|
|
121
|
+
'{"lastSeenSeq":0,"rounds":0,"withoutProgress":0,"lastRoundAt":null,"lastError":null}'
|
|
122
|
+
);
|
|
123
|
+
const saveState = (goalId, state) =>
|
|
124
|
+
store.meta(`coordinator:${goalId}`, JSON.stringify(state));
|
|
66
125
|
|
|
67
126
|
// What this machine knows that the round needs, whole: the transcript, who can run, and progress since last time.
|
|
68
127
|
const facts = async (thread, state) => {
|
|
69
|
-
const posts = store
|
|
70
|
-
|
|
128
|
+
const posts = store
|
|
129
|
+
.roomPosts(thread.id, 0, 500)
|
|
130
|
+
.filter((post) => !QUIET_KINDS.has(post.kind));
|
|
131
|
+
const transcript = posts.slice(-TRANSCRIPT_POSTS).map((post) => ({
|
|
132
|
+
seq: post.seq,
|
|
133
|
+
author: post.author,
|
|
134
|
+
kind: post.kind,
|
|
135
|
+
text: post.text,
|
|
136
|
+
}));
|
|
71
137
|
const runs = store.threadRuns(thread.id);
|
|
72
138
|
const since = state.lastRoundAt ?? "";
|
|
73
|
-
const runsFinished = runs.filter(
|
|
74
|
-
|
|
75
|
-
|
|
139
|
+
const runsFinished = runs.filter(
|
|
140
|
+
(run) => run.reported_at && run.reported_at > since
|
|
141
|
+
).length;
|
|
142
|
+
const newResults = posts.filter(
|
|
143
|
+
(post) =>
|
|
144
|
+
post.seq > state.lastSeenSeq &&
|
|
145
|
+
post.author !== "user" &&
|
|
146
|
+
!ownPost(thread, post)
|
|
147
|
+
).length;
|
|
148
|
+
const failedJobs = runs
|
|
149
|
+
.filter((run) => run.status === "failed")
|
|
150
|
+
.slice(-5)
|
|
151
|
+
.map((run) => ({
|
|
152
|
+
agent: run.target_agent || "agent",
|
|
153
|
+
objective: run.objective.slice(0, 500),
|
|
154
|
+
result: String(run.result || "").slice(0, 4_000),
|
|
155
|
+
}));
|
|
76
156
|
const waiting = runs.filter(waitsForWindow);
|
|
77
157
|
// Every agent the person can ask, each with its card (cards.mjs); a coding agent only where the thread has a folder for it
|
|
78
158
|
// to work in, and Harness not while its part here waits for a window.
|
|
79
|
-
|
|
159
|
+
// Each card's receipts go only to a Harness that takes them (the same release as frames); an older one would refuse the round.
|
|
160
|
+
const framesTaken = mode() === "local" || serverHas("swarm_frame");
|
|
161
|
+
const candidates = roster()
|
|
162
|
+
.filter(
|
|
163
|
+
(candidate) =>
|
|
164
|
+
(!CODING.has(candidate.id) || thread.goal.workspace) &&
|
|
165
|
+
!(candidate.id === "harness" && waiting.length)
|
|
166
|
+
)
|
|
167
|
+
.map((candidate) =>
|
|
168
|
+
framesTaken || !candidate.record?.checks
|
|
169
|
+
? candidate
|
|
170
|
+
: { ...candidate, record: { ...candidate.record, checks: undefined } }
|
|
171
|
+
);
|
|
80
172
|
// Earlier words close in meaning to the newest post that the transcript no longer carries, from this thread and the
|
|
81
173
|
// person's sessions on this machine.
|
|
82
|
-
const newest = posts
|
|
174
|
+
const newest = posts
|
|
175
|
+
.filter((post) => post.seq > state.lastSeenSeq)
|
|
176
|
+
.map((post) => post.text)
|
|
177
|
+
.join("\n")
|
|
178
|
+
.slice(0, 2_000);
|
|
83
179
|
const shown = new Set(transcript.map((post) => post.text));
|
|
84
180
|
let related = [];
|
|
85
181
|
if (newest.trim()) {
|
|
86
|
-
const found = await searchContext(thread.id, newest, 6).catch(() => ({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
182
|
+
const found = await searchContext(thread.id, newest, 6).catch(() => ({
|
|
183
|
+
entries: [],
|
|
184
|
+
}));
|
|
185
|
+
related = found.entries
|
|
186
|
+
.filter((entry) => !shown.has(entry.text))
|
|
187
|
+
.slice(0, 4)
|
|
188
|
+
.map((entry) => ({
|
|
189
|
+
source: `this thread, ${entry.author}`,
|
|
190
|
+
text: entry.text.slice(0, 4_000),
|
|
191
|
+
}));
|
|
192
|
+
for (const hit of store.search({
|
|
193
|
+
query: newest.split(/\s+/).slice(0, 12).join(" "),
|
|
194
|
+
limit: 3,
|
|
195
|
+
})) {
|
|
196
|
+
if (hit.excerpt)
|
|
197
|
+
related.push({
|
|
198
|
+
source: `${nameOf(hit.agent)} session "${String(hit.title || "").slice(0, 120)}"`,
|
|
199
|
+
text: String(hit.excerpt).slice(0, 4_000),
|
|
200
|
+
});
|
|
90
201
|
}
|
|
91
202
|
}
|
|
203
|
+
// The thread's task frame (frames.mjs): each run's receipt, other threads in the repository, its state.
|
|
204
|
+
const framed = frame(thread.id);
|
|
92
205
|
return {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
206
|
+
...(framed ? { frame: framed } : {}),
|
|
207
|
+
thread: {
|
|
208
|
+
id: thread.id,
|
|
209
|
+
title: thread.objective,
|
|
210
|
+
folder: thread.goal.workspace,
|
|
211
|
+
agents: thread.goal.team.filter(
|
|
212
|
+
(agent) => agent !== thread.goal.facilitator
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
transcript,
|
|
216
|
+
lastSeenSeq: state.lastSeenSeq,
|
|
217
|
+
candidates,
|
|
218
|
+
progress: {
|
|
219
|
+
runsFinished,
|
|
220
|
+
newResults,
|
|
221
|
+
stillRunning: runs.filter(going).length,
|
|
222
|
+
roundsWithoutProgress: state.withoutProgress,
|
|
223
|
+
failedJobs,
|
|
96
224
|
// A Harness that does not know the field would refuse the round: it goes only to one that takes Harness's tasks.
|
|
97
|
-
...(waiting.length && (mode() === "local" || serverHas("harness_task"))
|
|
225
|
+
...(waiting.length && (mode() === "local" || serverHas("harness_task"))
|
|
226
|
+
? {
|
|
227
|
+
waitingForWindow: waiting
|
|
228
|
+
.slice(-10)
|
|
229
|
+
.map((run) => ({ objective: run.objective.slice(0, 500) })),
|
|
230
|
+
}
|
|
231
|
+
: {}),
|
|
232
|
+
},
|
|
98
233
|
related: related.slice(0, 8),
|
|
99
234
|
};
|
|
100
235
|
};
|
|
@@ -102,16 +237,52 @@ export function createCoordinator({ store, mode, askHarness, connected, serverHa
|
|
|
102
237
|
// Carry a decision out under the thread's rules. A brief from the coordinator is agent-written work inside the
|
|
103
238
|
// thread, so it runs on the thread's authority the way a goal's work does; everything else only posts.
|
|
104
239
|
const apply = (thread, decision, coordinator, candidates) => {
|
|
105
|
-
if (decision.openItem)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
240
|
+
if (decision.openItem)
|
|
241
|
+
store.roomPost({
|
|
242
|
+
goalId: thread.id,
|
|
243
|
+
author: coordinator,
|
|
244
|
+
kind: "open_item",
|
|
245
|
+
text: String(decision.openItem).slice(0, 2_000),
|
|
246
|
+
});
|
|
247
|
+
if (decision.action === "pause") {
|
|
248
|
+
pauseGoal(
|
|
249
|
+
thread.id,
|
|
250
|
+
String(
|
|
251
|
+
decision.text || "the coordinator could not go on without you"
|
|
252
|
+
).slice(0, 300)
|
|
253
|
+
);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (decision.action === "ask_person" && decision.text)
|
|
257
|
+
store.roomPost({
|
|
258
|
+
goalId: thread.id,
|
|
259
|
+
author: coordinator,
|
|
260
|
+
kind: "question",
|
|
261
|
+
text: String(decision.text).slice(0, 4_000),
|
|
262
|
+
});
|
|
263
|
+
if (
|
|
264
|
+
decision.action === "hand_on" &&
|
|
265
|
+
candidates.some((candidate) => candidate.id === decision.agent) &&
|
|
266
|
+
decision.text
|
|
267
|
+
) {
|
|
268
|
+
if (!thread.goal.team.includes(decision.agent))
|
|
269
|
+
store.setGoalTeam(thread.id, [...thread.goal.team, decision.agent]);
|
|
270
|
+
const review =
|
|
271
|
+
decision.mode === "review" && REVIEWERS.has(decision.agent);
|
|
111
272
|
const text = String(decision.text).slice(0, 4_000);
|
|
112
273
|
// The brief is posted for everyone to read and the agent is woken here, so a review runs with its read-only switch.
|
|
113
|
-
store.roomPost({
|
|
114
|
-
|
|
274
|
+
store.roomPost({
|
|
275
|
+
goalId: thread.id,
|
|
276
|
+
author: coordinator,
|
|
277
|
+
kind: "post",
|
|
278
|
+
text: `@${decision.agent} ${review ? "(review, no changes) " : ""}${text}`,
|
|
279
|
+
});
|
|
280
|
+
handOn(
|
|
281
|
+
thread.id,
|
|
282
|
+
decision.agent,
|
|
283
|
+
`${coordinator} asked you: ${text.slice(0, 300)}`,
|
|
284
|
+
{ review, words: text }
|
|
285
|
+
);
|
|
115
286
|
}
|
|
116
287
|
notifyBoard(store.getTask(thread.id));
|
|
117
288
|
};
|
|
@@ -120,52 +291,106 @@ export function createCoordinator({ store, mode, askHarness, connected, serverHa
|
|
|
120
291
|
const thread = store.getTask(goalId);
|
|
121
292
|
if (!thread || thread.kind !== "goal" || thread.status !== "open") return;
|
|
122
293
|
const state = stateOf(goalId);
|
|
123
|
-
const posts = store
|
|
294
|
+
const posts = store
|
|
295
|
+
.roomPosts(goalId, state.lastSeenSeq, 500)
|
|
296
|
+
.filter((post) => !QUIET_KINDS.has(post.kind) && !ownPost(thread, post));
|
|
124
297
|
const active = store.threadRuns(goalId).some(going);
|
|
125
298
|
if (!posts.length || active) return;
|
|
126
299
|
// A task's own room is coordinated once it becomes a conversation: someone posted in it, not only the task and its result.
|
|
127
|
-
if (
|
|
300
|
+
if (
|
|
301
|
+
store.meta(`task-room:${goalId}`) &&
|
|
302
|
+
!store.roomPosts(goalId, 0, 500).some((post) => post.kind === "post")
|
|
303
|
+
)
|
|
304
|
+
return;
|
|
128
305
|
// A thread started through Harness by an agent that runs elsewhere gets no rounds until the person sets a spend ceiling.
|
|
129
306
|
if (store.meta(`hosted-thread:${goalId}`)) return;
|
|
130
307
|
const local = mode() === "local";
|
|
131
308
|
if (!local && !connected()) return;
|
|
132
|
-
const localAgent = local
|
|
309
|
+
const localAgent = local
|
|
310
|
+
? localAgents().find(
|
|
311
|
+
(agent) => agent === "claude-code" || agent === "codex"
|
|
312
|
+
)
|
|
313
|
+
: null;
|
|
133
314
|
if (local && !localAgent) return;
|
|
134
315
|
const gathered = await facts(thread, state);
|
|
135
|
-
const lastSeq =
|
|
136
|
-
|
|
316
|
+
const lastSeq =
|
|
317
|
+
store.roomPosts(goalId, 0, 500).at(-1)?.seq ?? state.lastSeenSeq;
|
|
318
|
+
const progressed =
|
|
319
|
+
gathered.progress.runsFinished > 0 || gathered.progress.newResults > 0;
|
|
137
320
|
const startedAt = Date.now();
|
|
138
|
-
let decision = null,
|
|
321
|
+
let decision = null,
|
|
322
|
+
error = null;
|
|
139
323
|
try {
|
|
140
324
|
if (local) decision = await localDecision(localAgent, gathered);
|
|
141
325
|
else {
|
|
142
326
|
// No deadline of its own: the round ends when Harness answers or the connection drops.
|
|
143
327
|
const reply = await askHarness("coordinate", gathered, null);
|
|
144
|
-
if (!reply.ok)
|
|
328
|
+
if (!reply.ok)
|
|
329
|
+
throw new Error(
|
|
330
|
+
reply.error || "Harness did not coordinate this round"
|
|
331
|
+
);
|
|
145
332
|
decision = reply.result ?? reply;
|
|
146
333
|
}
|
|
147
|
-
} catch (failure) {
|
|
148
|
-
|
|
334
|
+
} catch (failure) {
|
|
335
|
+
error = failure.message;
|
|
336
|
+
}
|
|
337
|
+
const next = {
|
|
338
|
+
...state,
|
|
339
|
+
lastSeenSeq: lastSeq,
|
|
340
|
+
rounds: state.rounds + 1,
|
|
341
|
+
withoutProgress: progressed ? 0 : state.withoutProgress + 1,
|
|
342
|
+
lastRoundAt: new Date().toISOString(),
|
|
343
|
+
lastError: error,
|
|
344
|
+
};
|
|
149
345
|
saveState(goalId, next);
|
|
150
|
-
store.recordRound({
|
|
151
|
-
|
|
346
|
+
store.recordRound({
|
|
347
|
+
goalId,
|
|
348
|
+
coordinator: local ? localAgent : "harness",
|
|
349
|
+
action: decision?.action ?? null,
|
|
350
|
+
agent: decision?.agent ?? null,
|
|
351
|
+
loop: decision?.loop ?? null,
|
|
352
|
+
judgments: decision?.judgments ?? null,
|
|
353
|
+
progress: gathered.progress,
|
|
354
|
+
estimatedMicroUsd: decision?.estimatedMicroUsd ?? null,
|
|
355
|
+
elapsedMs: Date.now() - startedAt,
|
|
356
|
+
error,
|
|
357
|
+
});
|
|
152
358
|
if (error) {
|
|
153
359
|
log(`coordinator round for ${goalId}: ${error}`);
|
|
154
|
-
if (error !== state.lastError)
|
|
360
|
+
if (error !== state.lastError)
|
|
361
|
+
store.roomPost({
|
|
362
|
+
goalId,
|
|
363
|
+
author: thread.goal.facilitator,
|
|
364
|
+
kind: "note",
|
|
365
|
+
text: `Harness could not coordinate: ${error}`,
|
|
366
|
+
});
|
|
155
367
|
notifyBoard(store.getTask(goalId));
|
|
156
368
|
return;
|
|
157
369
|
}
|
|
158
|
-
apply(
|
|
370
|
+
apply(
|
|
371
|
+
store.getTask(goalId),
|
|
372
|
+
decision,
|
|
373
|
+
thread.goal.facilitator,
|
|
374
|
+
gathered.candidates
|
|
375
|
+
);
|
|
159
376
|
};
|
|
160
377
|
|
|
161
378
|
/** A thread changed: run a round once it is quiet; one at a time per thread, again if more came in meanwhile. */
|
|
162
379
|
const poke = (goalId) => {
|
|
163
|
-
if (busy.has(goalId)) {
|
|
380
|
+
if (busy.has(goalId)) {
|
|
381
|
+
again.add(goalId);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
164
384
|
busy.add(goalId);
|
|
165
|
-
setImmediate(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
385
|
+
setImmediate(
|
|
386
|
+
() =>
|
|
387
|
+
void round(goalId)
|
|
388
|
+
.catch((failure) => log(`coordinator ${goalId}: ${failure.message}`))
|
|
389
|
+
.finally(() => {
|
|
390
|
+
busy.delete(goalId);
|
|
391
|
+
if (again.delete(goalId)) poke(goalId);
|
|
392
|
+
})
|
|
393
|
+
);
|
|
169
394
|
};
|
|
170
395
|
return { poke, state: stateOf };
|
|
171
396
|
}
|