tryharness 0.7.18 → 0.7.22
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 +9 -6
- package/assets/opencode-plugin.js +46 -66
- package/assets/pi-extension.ts +90 -90
- package/cli.mjs +31 -1
- package/lib/adapters.mjs +19 -11
- package/lib/agentnode.mjs +154 -0
- package/lib/cards.mjs +11 -3
- package/lib/coordinator.mjs +19 -9
- package/lib/daemon.mjs +226 -57
- package/lib/folders.mjs +138 -0
- package/lib/live.mjs +30 -5
- package/lib/mcp-stdio.mjs +10 -2
- package/lib/members.json +1 -1
- package/lib/models.mjs +233 -0
- package/lib/network.mjs +118 -0
- package/lib/nudge.mjs +6 -5
- package/lib/phones.mjs +5 -1
- package/lib/places.mjs +45 -0
- package/lib/runner.mjs +35 -6
- package/lib/store.mjs +32 -3
- package/lib/swarm.mjs +84 -23
- package/lib/t3.mjs +7 -4
- package/lib/worktrees.mjs +21 -5
- package/package.json +3 -3
package/lib/daemon.mjs
CHANGED
|
@@ -13,9 +13,10 @@ import { parseSessionFile } from "./parsers.mjs";
|
|
|
13
13
|
import { historyRoots, paths } from "./paths.mjs";
|
|
14
14
|
import { openStore } from "./store.mjs";
|
|
15
15
|
import { readRunSteps, runAgentTask, runningPids, stopAgentTask } from "./runner.mjs";
|
|
16
|
-
import { createSwarm, nativeSessionId } from "./swarm.mjs";
|
|
16
|
+
import { PROCESS_NAMES, createSwarm, nativeSessionId } from "./swarm.mjs";
|
|
17
17
|
import { createCoordinator } from "./coordinator.mjs";
|
|
18
18
|
import { createWorktrees } from "./worktrees.mjs";
|
|
19
|
+
import { createFolders } from "./folders.mjs";
|
|
19
20
|
import { notificationFor, notifyDesktop } from "./notify.mjs";
|
|
20
21
|
import { secretReason } from "./secrets.mjs";
|
|
21
22
|
import { provenanceFor, provenanceLine } from "./provenance.mjs";
|
|
@@ -27,11 +28,13 @@ import { ASKED_BY_TASK, CLOUD_HOMED, MACHINE_RUNNERS, TARGETS, aboutOf, member,
|
|
|
27
28
|
import { fingerprint, openBound, openBytes, relayKey, sealBound, sealBytes } from "./sealed.mjs";
|
|
28
29
|
import { claudeMemNotes } from "./claudemem.mjs";
|
|
29
30
|
import { machineActivity } from "./activity.mjs";
|
|
30
|
-
import { LIVE_AGENTS, focusTerminal, hostApp, liveSessions, processTable, sessionFeed, sessionFile, sessionOfProcess, workingDirectories } from "./live.mjs";
|
|
31
|
-
import { EDITOR_IDS, agentCommand, editorFolder, editorInstall, editorName, editorTerminalStarts, openEditorFolder, panelLink, placeEnv, placeFrom, placeKey, processEnv, resumeLine, t3App, t3Link, t3Model, t3Open, t3OpensLinks, t3Project, t3Provider, t3Runs, t3Thread } from "./places.mjs";
|
|
31
|
+
import { LIVE_AGENTS, focusTerminal, hostApp, liveSessions, opencodeFeed, processTable, sessionFeed, sessionFile, sessionOfProcess, workingDirectories } from "./live.mjs";
|
|
32
|
+
import { EDITOR_IDS, agentCommand, editorFolder, editorInstall, editorName, editorRunsOpenTasks, editorSessionWorkspace, editorTerminalStarts, openEditorFolder, panelLink, placeEnv, placeFrom, placeKey, processEnv, resumeLine, t3App, t3Link, t3Model, t3Open, t3OpensLinks, t3Project, t3Provider, t3Runs, t3Thread } from "./places.mjs";
|
|
32
33
|
import { detectAgentApps, detectMcpClients } from "./adapters.mjs";
|
|
33
|
-
import { cardLine, swarmBlock, swarmCards, swarmDigest } from "./cards.mjs";
|
|
34
|
+
import { cardLine, modelOf, swarmBlock, swarmCards, swarmDigest } from "./cards.mjs";
|
|
35
|
+
import { PICKABLE, createModelRouter, picksIn, refreshCatalogs, resolvePick, setRunPick } from "./models.mjs";
|
|
34
36
|
import { forgetT3, handedToT3, runInT3, sendToT3, startT3SignIn, t3Access } from "./t3.mjs";
|
|
37
|
+
import { createNetwork } from "./network.mjs";
|
|
35
38
|
|
|
36
39
|
// What may be referenced on a thread with another person (the server's rule, src/server/relay/refs.ts, kept
|
|
37
40
|
// here since a sealed request is opened only on the machines).
|
|
@@ -301,6 +304,8 @@ export async function startDaemon() {
|
|
|
301
304
|
try { if (fs.existsSync(paths.socket)) fs.unlinkSync(paths.socket); } catch {}
|
|
302
305
|
}
|
|
303
306
|
let socket = null;
|
|
307
|
+
// What the Harness this machine is connected to says it can do, told when the connection opens.
|
|
308
|
+
let serverFeatures = new Set();
|
|
304
309
|
const relayWaiters = new Map();
|
|
305
310
|
const cloudSeen = new Map();
|
|
306
311
|
const send = (message) => {
|
|
@@ -464,15 +469,15 @@ export async function startDaemon() {
|
|
|
464
469
|
// as themselves, never as a root. Anything another agent wrote never starts. Links are followed, so no path inside a
|
|
465
470
|
// covered folder leads out of it.
|
|
466
471
|
const RUNNERS = new Set(MACHINE_RUNNERS);
|
|
467
|
-
|
|
468
|
-
//
|
|
472
|
+
// A session the person typed in: its prompt hook (Claude Code, Codex) or its extension or plugin (pi, OpenCode) names
|
|
473
|
+
// the agent process, the parent of the board MCP it posts through.
|
|
469
474
|
// A run this daemon started is never one (the daemon is its parent), and an agent holding another agent's task writes
|
|
470
475
|
// as an agent until it reports.
|
|
471
476
|
const promptedBy = new Map();
|
|
472
477
|
// Unknown parent: not the person's.
|
|
473
478
|
const parentOf = (pid) => Number(spawnSync(PS, ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" }).stdout?.trim()) || null;
|
|
474
479
|
const personSession = (agent, pid) => {
|
|
475
|
-
if (!
|
|
480
|
+
if (!RUNNERS.has(agent) || !Number.isInteger(pid) || promptedBy.get(pid) !== agent) return false;
|
|
476
481
|
const parent = parentOf(pid);
|
|
477
482
|
return parent !== null && parent !== process.pid && !store.holdsAgentTask(agent);
|
|
478
483
|
};
|
|
@@ -586,12 +591,18 @@ export async function startDaemon() {
|
|
|
586
591
|
};
|
|
587
592
|
// Work a thread starts on its own runs on the thread's authority only when the person gave it: a thread they set, or,
|
|
588
593
|
// for a task's room, their go on that task. Anything else waits for the person's Start like any agent-written run.
|
|
589
|
-
//
|
|
590
|
-
//
|
|
594
|
+
// In such a thread, the person's own agents asking for work carry the person's go, as their @mention does (owner,
|
|
595
|
+
// October 13; docs/member-runs.md): Harness and its agents on its server (Bankr, Quotient), and the coding agents on
|
|
596
|
+
// this computer; full permissions, Stop always there. An agent the person set to wait still waits. Work another agent
|
|
597
|
+
// asked for (Grok Bot, Muse, another person's agent, a call through Harness) runs on a goal's authority only where it
|
|
598
|
+
// can be held in: Claude Code in restricted mode, in a copy of the folder this daemon made. Anything else waits.
|
|
591
599
|
const worktreeRoot = path.join(paths.home, "worktrees");
|
|
592
600
|
const restrictable = (agent, folder) => agent === "claude-code" && Boolean(folder) && within(folder, worktreeRoot);
|
|
593
|
-
const
|
|
594
|
-
|
|
601
|
+
const setToWait = (agent) => standingRules().find((rule) => rule.agent === agent)?.mode === "off";
|
|
602
|
+
const personsGo = (asker, agent, { hosted = false } = {}) => asker === "user"
|
|
603
|
+
|| ((member(asker)?.runs === "server" || (!hosted && member(asker)?.runs === "machine")) && !setToWait(agent));
|
|
604
|
+
const restrictedWait = (agent) => setToWait(agent) ? `${nameOf(agent)} is set to wait for your Start.`
|
|
605
|
+
: agent === "claude-code" ? `${nameOf(agent)} would work in your own folder here, not a copy, so work another agent asked for waits for your Start.`
|
|
595
606
|
: `${nameOf(agent)} has no restricted mode, so work another agent asked of it waits for your Start.`;
|
|
596
607
|
const threadMayRun = (goal) => {
|
|
597
608
|
if (goal.trust !== "user") return false;
|
|
@@ -608,19 +619,47 @@ export async function startDaemon() {
|
|
|
608
619
|
if (review || !t3Provider(agent) || !thread.goal.workspace) return false;
|
|
609
620
|
const tree = worktrees.get(thread.id);
|
|
610
621
|
if (tree?.state === "t3") return true;
|
|
611
|
-
|
|
612
|
-
|
|
622
|
+
// A task's room works in place, which T3 does in its project's own checkout; a thread carrying on a terminal
|
|
623
|
+
// session in place resumes that session here.
|
|
624
|
+
const movable = !tree || (tree.state === "open" && fs.existsSync(tree.path)) || (tree.state === "in_place" && Boolean(store.meta(`task-room:${thread.id}`)));
|
|
625
|
+
return movable && homeOf(agent)?.kind === "t3" && !t3Gap(agent, thread.goal.workspace);
|
|
626
|
+
};
|
|
627
|
+
// Why an agent the person uses in T3 Code would not run there in a folder; null when it would, or when its home is not T3.
|
|
628
|
+
const t3Gap = (agent, folder) => {
|
|
629
|
+
if (!t3Provider(agent) || homeOf(agent)?.kind !== "t3") return null;
|
|
630
|
+
if (t3Access()?.state !== "send") return "Harness isn't approved to send into T3 Code";
|
|
631
|
+
if (!t3Open()) return "T3 Code isn't open";
|
|
632
|
+
if (!t3Project(folder)) return `T3 Code has no project for ${path.basename(folder)}`;
|
|
633
|
+
if (!t3Model(agent)) return `${nameOf(agent)} hasn't run in T3 Code yet`;
|
|
634
|
+
return null;
|
|
635
|
+
};
|
|
636
|
+
// Where an agent's turns in a folder run, for the composer: in T3 Code, or here, with why not T3 when it is the agent's home.
|
|
637
|
+
const runsIn = (agent, folder) => {
|
|
638
|
+
const gap = t3Gap(agent, folder);
|
|
639
|
+
return homeOf(agent)?.kind === "t3" && !gap ? { in: "t3", why: null } : { in: "here", why: gap };
|
|
640
|
+
};
|
|
641
|
+
// A task's own run (its room's first job) goes where its room's turns go: into T3 Code when the person uses the agent
|
|
642
|
+
// there. Not when it would run restricted (work another agent asked for on a goal's authority): only this computer
|
|
643
|
+
// holds a run to that.
|
|
644
|
+
const taskInT3 = (task, agent) => {
|
|
645
|
+
const room = task.parent_id ? store.getTask(task.parent_id) : null;
|
|
646
|
+
if (task.kind !== "task" || room?.kind !== "goal" || store.meta(`task-room:${room.id}`) !== task.id || !task.run_workspace) return false;
|
|
647
|
+
const restricted = task.trust !== "user" && task.run_authorized_by === "goal" && store.meta(`person-wake:${task.id}`) !== "1";
|
|
648
|
+
return !restricted && inT3(room, agent, store.meta(`review:${task.id}`) === "1");
|
|
613
649
|
};
|
|
614
650
|
// A turn T3 carries out (t3.mjs runInT3): into the agent's T3 thread in this thread, else a new one in T3's project
|
|
615
651
|
// for the folder, on the thread's T3 checkout. The thread works in T3 from its first launch.
|
|
616
|
-
|
|
652
|
+
// A model the relay chose goes to T3 as its selection, effort under the option name T3 gives each agent.
|
|
653
|
+
const t3Turn = (task, agent, chosen) => {
|
|
617
654
|
const thread = store.getTask(task.parent_id);
|
|
618
655
|
const key = `t3-thread:${thread.id}:${agent}`;
|
|
619
656
|
const model = t3Model(agent);
|
|
620
657
|
const project = t3Project(thread.goal.workspace);
|
|
658
|
+
const effort = chosen.effort ? [{ id: agent === "codex" ? "reasoningEffort" : "effort", value: chosen.effort }] : [];
|
|
621
659
|
return {
|
|
622
660
|
threadId: store.meta(key) || null,
|
|
623
661
|
title: thread.objective.slice(0, 120),
|
|
662
|
+
model: chosen.model ? { instanceId: t3Provider(agent), model: chosen.model, options: effort } : null,
|
|
624
663
|
launch: { ...(project ? { projectId: project.id } : {}), ...(model ? { modelSelection: model } : {}), workspaceStrategy: worktrees.t3Workspace(thread) },
|
|
625
664
|
onThread: (threadId) => { store.meta(key, threadId); worktrees.inT3(thread.id); },
|
|
626
665
|
onWorktree: (where, branch) => worktrees.inT3(thread.id, where, branch),
|
|
@@ -629,33 +668,46 @@ export async function startDaemon() {
|
|
|
629
668
|
// Wake one teammate. A coding agent here gets a run in the goal's folder that resumes its last session on this goal;
|
|
630
669
|
// Harness and agents that cannot be started here learn of it through the board change and their own reads.
|
|
631
670
|
const pendingWakes = new Map();
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
|
|
671
|
+
// go: the person's go for this turn (personsGo), their own post naming this agent or one of their agents here asking;
|
|
672
|
+
// any other wake is work another agent asked for and runs restricted or waits (docs/member-runs.md).
|
|
673
|
+
// asker: who asked for this turn, the person or an agent, named on the card that waits for the person.
|
|
674
|
+
// pick: the model and effort the post named for this turn (models.mjs).
|
|
675
|
+
const wakeAgent = (goalId, agent, reason, { review = false, go = false, asker = "harness", pick = null } = {}) => {
|
|
635
676
|
const task = store.getTask(goalId);
|
|
636
677
|
if (!task || task.status !== "open" || !RUNNERS.has(agent) || !installedAgents.includes(agent)) return;
|
|
637
678
|
const goal = task.goal;
|
|
638
679
|
if (!goal.workspace) {
|
|
639
680
|
if (!store.meta(`goal-folder-note:${goalId}:${agent}`)) {
|
|
640
681
|
store.meta(`goal-folder-note:${goalId}:${agent}`, "1");
|
|
641
|
-
store.roomPost({ goalId, author: "harness", kind: "note", text: `${nameOf(agent)} cannot start: this thread has no folder to work in.
|
|
682
|
+
store.roomPost({ goalId, author: "harness", kind: "note", text: `${nameOf(agent)} cannot start: this thread has no folder to work in. Choose one for the thread in Harness, or: npx tryharness goal folder ${goalId} <dir>` });
|
|
642
683
|
}
|
|
643
684
|
return;
|
|
644
685
|
}
|
|
645
|
-
if (store.openWake(goalId, agent)) { pendingWakes.set(`${goalId}:${agent}`, { reason,
|
|
686
|
+
if (store.openWake(goalId, agent)) { pendingWakes.set(`${goalId}:${agent}`, { reason, asker, go: go || Boolean(pendingWakes.get(`${goalId}:${agent}`)?.go), pick: pick ?? pendingWakes.get(`${goalId}:${agent}`)?.pick ?? null }); return; }
|
|
646
687
|
if (store.wakeCount(goalId, agent) >= goal.max_wakes) { pauseGoal(goalId, `${agent} was woken ${goal.max_wakes} times`); return; }
|
|
647
688
|
const brief = swarmBrief(task, agent, reason, { review });
|
|
648
689
|
// A coding thread in a repository works in its own worktree, so threads never share a checkout; one whose agents the
|
|
649
690
|
// person uses in T3 Code works in T3 (inT3), where T3 makes and keeps the checkout.
|
|
650
691
|
const t3 = inT3(task, agent, review);
|
|
692
|
+
// The person uses this agent in T3 Code but this turn runs here: the thread says why, once for each reason.
|
|
693
|
+
const gap = t3 || review ? null : t3Gap(agent, goal.workspace);
|
|
694
|
+
if (gap && !store.meta(`t3-gap-note:${goalId}:${agent}:${gap}`)) {
|
|
695
|
+
store.meta(`t3-gap-note:${goalId}:${agent}:${gap}`, "1");
|
|
696
|
+
store.roomPost({ goalId, author: "harness", kind: "note", text: `${nameOf(agent)} is working on this computer, not in T3 Code: ${gap}.` });
|
|
697
|
+
}
|
|
651
698
|
let folder = goal.workspace;
|
|
652
699
|
if (!t3) try { folder = worktrees.folderFor(task); } catch (error) { store.roomPost({ goalId, author: "harness", kind: "note", text: `${agent} cannot start: ${error.message}` }); notifyBoard(store.getTask(goalId)); return; }
|
|
653
|
-
|
|
700
|
+
// Whose words the turn carries: whoever wrote the thread when an agent did, then the agent that asked. The person's
|
|
701
|
+
// own ask in their own thread names no one; Harness posts every wake.
|
|
702
|
+
const writers = task.trust === "agent" ? task.origin_chain : [];
|
|
703
|
+
const chain = asker === "user" || writers.at(-1) === asker ? writers : [...writers, asker];
|
|
704
|
+
const wake = store.postTask({ objective: `Room: ${task.objective}`.slice(0, 500), brief, originAgent: "harness", provenance: { trust: "agent", chain: chain.slice(-16) }, targetAgent: agent, parentId: goalId, runWorkspace: folder, kind: "wake" });
|
|
654
705
|
if (t3) store.meta(`t3-run:${wake.id}`, "1");
|
|
706
|
+
if (pick) setRunPick(store, wake.id, pick, asker);
|
|
655
707
|
// A review runs with the agent's own read-only switch (runner.mjs); only Claude Code and Codex have one.
|
|
656
708
|
if (review) store.meta(`review:${wake.id}`, "1");
|
|
657
709
|
if (threadMayRun(task)) {
|
|
658
|
-
if (
|
|
710
|
+
if (go) { store.meta(`person-wake:${wake.id}`, "1"); store.authorizeGoalRun(wake.id); }
|
|
659
711
|
else if (restrictable(agent, folder)) store.authorizeGoalRun(wake.id);
|
|
660
712
|
else if (!store.meta(`restricted-note:${goalId}:${agent}`)) {
|
|
661
713
|
store.meta(`restricted-note:${goalId}:${agent}`, "1");
|
|
@@ -664,9 +716,13 @@ export async function startDaemon() {
|
|
|
664
716
|
}
|
|
665
717
|
notifyBoard(store.getTask(wake.id));
|
|
666
718
|
};
|
|
719
|
+
// Harness asked by name answers from the person's open Harness window with its own tools (src/main/agents/harnessInbox.ts):
|
|
720
|
+
// the words whole and where to read the rest. With no window open the ask waits on the board.
|
|
721
|
+
const harnessAsk = (task, words) => `Asked in the thread "${task.objective}" (thread ${task.id}):\n\n${words}\n\n`
|
|
722
|
+
+ `Read the thread with read_goal_room (id ${task.id}) before you answer. Your answer here goes back into the thread.`;
|
|
667
723
|
// Ask an agent that is not started here: the ask is a task in the thread, posted in the asker's name, so its
|
|
668
724
|
// authority is the asker's (an agent's ask to Bankr waits for the person). One open ask per agent per thread.
|
|
669
|
-
const askAgent = (goalId, agent, reason, asker) => {
|
|
725
|
+
const askAgent = (goalId, agent, reason, asker, words = reason) => {
|
|
670
726
|
const task = store.getTask(goalId);
|
|
671
727
|
if (!task || task.status !== "open") return;
|
|
672
728
|
if (store.threadRuns(goalId).some((run) => run.target_agent === agent && ["open", "claimed", "running", "needs_input"].includes(run.status))) return;
|
|
@@ -675,18 +731,48 @@ export async function startDaemon() {
|
|
|
675
731
|
// with money (Bankr, Quotient) waits for the person under its own rules.
|
|
676
732
|
const written = from === "user" ? {} : { provenance: { trust: "agent", chain: [...(task.origin_chain || []), from].slice(-16) } };
|
|
677
733
|
const posted = boardCommand({ command: "board.post", agent: from, objective: `Asked in "${task.objective}": ${reason}`.slice(0, 500),
|
|
678
|
-
brief: swarmBrief(task, agent, reason, { ask: true }), targetAgent: agent, parentId: goalId, ...written });
|
|
734
|
+
brief: agent === "harness" ? harnessAsk(task, words) : swarmBrief(task, agent, reason, { ask: true }), targetAgent: agent, parentId: goalId, ...written });
|
|
679
735
|
const failed = (error) => { store.roomPost({ goalId, author: task.goal.facilitator, kind: "note", text: `${nameOf(agent)} could not be asked: ${error}` }); notifyBoard(store.getTask(goalId)); };
|
|
680
736
|
if (posted?.pending) posted.pending.catch((error) => failed(error.message));
|
|
681
737
|
else if (posted && !posted.ok) failed(posted.error);
|
|
738
|
+
else if (agent === "harness" && posted?.task) offerToServer(task, posted.task.id, from, words);
|
|
682
739
|
};
|
|
683
|
-
|
|
740
|
+
// Harness's server takes a task given to Harness in a thread while no Harness window is open (src/server/swarm/harnessTask.ts),
|
|
741
|
+
// only while this computer's coordinator runs on Harness: on the privacy setting the thread's words stay here and the task
|
|
742
|
+
// waits for a window. Harness holds it meanwhile, so a window that opens does not take it too; what the server leaves for
|
|
743
|
+
// the browser goes back on the board, and the window takes it over when it next opens (owner, October 14).
|
|
744
|
+
const offerToServer = (goal, taskId, asker, words) => {
|
|
745
|
+
if (coordinatorMode() !== "harness" || !state.connected || !serverFeatures.has("harness_task")) return;
|
|
746
|
+
if (!boardCommand({ command: "board.claim", agent: "harness", id: taskId })?.ok) return;
|
|
747
|
+
const transcript = store.roomPosts(goal.id, 0, 500).filter((post) => !["note", "held", "round"].includes(post.kind)).slice(-30)
|
|
748
|
+
.map((post) => ({ author: post.author, kind: post.kind, text: post.text.slice(0, 20_000) }));
|
|
749
|
+
// Back to the board for a window, as the person would put it back, with what the thread should hear.
|
|
750
|
+
const back = (post) => {
|
|
751
|
+
if (post) store.roomPost({ goalId: goal.id, author: "harness", ...post });
|
|
752
|
+
boardCommand({ command: "board.release", agent: "user", id: taskId });
|
|
753
|
+
notifyBoard(store.getTask(goal.id));
|
|
754
|
+
};
|
|
755
|
+
const waits = (why) => back({ kind: "note", text: `This waits for your Harness window: ${why}.` });
|
|
756
|
+
askHarness("harness_task", { taskId, thread: { id: goal.id, title: goal.objective.slice(0, 500) }, asker, words: String(words).slice(0, 20_000), transcript }, null).then((reply) => {
|
|
757
|
+
// askHarness spreads Harness's answer into the reply.
|
|
758
|
+
const outcome = reply?.ok ? reply : null;
|
|
759
|
+
if (!outcome) return waits(`Harness could not take it here (${reply?.error || "no answer"})`);
|
|
760
|
+
if (!outcome.taken) return outcome.why === "window_open" ? back(null) : waits(outcome.detail || "Harness could not take it here");
|
|
761
|
+
if (outcome.status === "done") return boardCommand({ command: "board.report", agent: "harness", id: taskId, status: "done", result: outcome.text });
|
|
762
|
+
if (outcome.status === "window") return back({ kind: "post", text: `${outcome.text}\n\nThe rest needs your browser: ${outcome.windowOpen ? "your Harness window is taking it over" : "Harness takes it over when your Harness window next opens"}.` });
|
|
763
|
+
waits(`Harness could not finish it here${outcome.text ? ` (${outcome.text.slice(0, 300)})` : ""}`);
|
|
764
|
+
}, (error) => waits(`Harness could not take it here (${error.message})`));
|
|
765
|
+
};
|
|
766
|
+
const wakeFor = (goalId, post, author, names = post.mentions, { hosted = false } = {}) => {
|
|
684
767
|
const goal = store.goal(goalId);
|
|
768
|
+
const { picks } = picksIn(store, post.text, names);
|
|
685
769
|
for (const agent of names) {
|
|
686
770
|
if (agent === author) continue;
|
|
687
771
|
const reason = `${author} mentioned you: ${post.text.slice(0, 300)}`;
|
|
688
|
-
if (RUNNERS.has(agent)) wakeAgent(goalId, agent, reason, {
|
|
772
|
+
if (RUNNERS.has(agent)) wakeAgent(goalId, agent, reason, { go: personsGo(author, agent, { hosted }), asker: author, pick: picks.get(agent) });
|
|
689
773
|
else if (ASKABLE.has(agent)) askAgent(goalId, agent, reason, author);
|
|
774
|
+
// Harness by name: the person's @harness or an agent's ask. A message naming no one falls to Harness as coordinator.
|
|
775
|
+
else if (agent === "harness" && post.kind === "post" && (author !== "user" || mentionsIn(post.text, [agent]).length)) askAgent(goalId, agent, reason, author, post.text);
|
|
690
776
|
}
|
|
691
777
|
// Harness follows through the board change; it is woken by mentions, results and questions, not by every note.
|
|
692
778
|
if (goal && names.includes(goal.facilitator)) notifyBoard(store.getTask(goalId));
|
|
@@ -700,7 +786,7 @@ export async function startDaemon() {
|
|
|
700
786
|
const stopped = kind === "failed" && store.getTask(goalId)?.status !== "open";
|
|
701
787
|
if (!stopped && !store.postsBy(goalId, task.target_agent, startedAt) && text) store.roomPost({ goalId, author: task.target_agent, kind: kind === "done" ? "post" : "failed", text: kind === "done" ? text : `run failed: ${text}` });
|
|
702
788
|
const pending = pendingWakes.get(`${goalId}:${task.target_agent}`);
|
|
703
|
-
if (pending) { pendingWakes.delete(`${goalId}:${task.target_agent}`); wakeAgent(goalId, task.target_agent, pending.reason, {
|
|
789
|
+
if (pending) { pendingWakes.delete(`${goalId}:${task.target_agent}`); wakeAgent(goalId, task.target_agent, pending.reason, { go: pending.go, asker: pending.asker, pick: pending.pick }); }
|
|
704
790
|
notifyBoard(store.getTask(goalId));
|
|
705
791
|
};
|
|
706
792
|
// A job under a goal changed: its result or question lands in the room and wakes whoever posted it.
|
|
@@ -711,8 +797,10 @@ export async function startDaemon() {
|
|
|
711
797
|
const who = task.claimed_by || task.target_agent || "an agent";
|
|
712
798
|
// A task's own job reports to whoever posted it the way any task does; it does not wake them.
|
|
713
799
|
const root = store.meta(`task-room:${goal.id}`) === task.id;
|
|
800
|
+
// An answer to an ask (askAgent) follows the ask in the thread, so it stands as said; a job's result names the job.
|
|
801
|
+
const answer = task.status !== "failed" && String(task.result || "").trim() && task.objective.startsWith(`Asked in "${goal.objective}": `);
|
|
714
802
|
const post = store.roomPost({ goalId: goal.id, author: who, kind: task.status === "needs_input" ? "question" : "result",
|
|
715
|
-
text: `${task.status === "done" ? "Finished" : task.status === "failed" ? "Could not finish" : "Asks about"} job "${task.objective}": ${task.result || ""}
|
|
803
|
+
text: (answer ? task.result : `${task.status === "done" ? "Finished" : task.status === "failed" ? "Could not finish" : "Asks about"} job "${task.objective}": ${task.result || ""}`).slice(0, 20_000),
|
|
716
804
|
mentions: root ? [] : [task.origin_agent].filter((agent) => goal.goal.team.includes(agent)) });
|
|
717
805
|
// A room where only the task's own agent worked closes with the task; one that grew into a conversation stays open.
|
|
718
806
|
if (root && task.status !== "needs_input" && goal.status === "open" && !store.roomPosts(goal.id, 0, 500).some((entry) => entry.kind === "post")
|
|
@@ -768,6 +856,9 @@ export async function startDaemon() {
|
|
|
768
856
|
if (goal.status !== "open") return { ok: false, error: `the goal is ${goal.status}` };
|
|
769
857
|
const text = String(request.text || "").trim().slice(0, 20_000);
|
|
770
858
|
if (!text) return { ok: false, error: "a post needs text" };
|
|
859
|
+
// A model named for an agent (`@codex:gpt-6-luna/high`) must be one it lists, or the post is refused saying why.
|
|
860
|
+
const picked = picksIn(store, text, ROOM_AGENTS);
|
|
861
|
+
if (picked.error) return { ok: false, error: picked.error };
|
|
771
862
|
const flag = secretReason(text);
|
|
772
863
|
if (flag) {
|
|
773
864
|
const held = store.roomPost({ goalId: goal.id, author: agent, kind: "held", text });
|
|
@@ -796,7 +887,7 @@ export async function startDaemon() {
|
|
|
796
887
|
if (sent.ok && sent.message.state === "sent") sendThreadMessage(sent.message);
|
|
797
888
|
}
|
|
798
889
|
if (store.roomCount(goal.id) >= goal.goal.max_posts) { pauseGoal(goal.id, `the room reached ${goal.goal.max_posts} posts`); return { ok: true, post, paused: true }; }
|
|
799
|
-
wakeFor(goal.id, post, agent, wakes);
|
|
890
|
+
wakeFor(goal.id, post, agent, wakes, { hosted: request.hostedCaller === true });
|
|
800
891
|
notifyBoard(store.getTask(goal.id));
|
|
801
892
|
const notes = [...(unwoken.length ? [`Named ${unwoken.map((name) => `@${name}`).join(", ")} without asking, so not woken. To ask an agent, put its name in ask.`] : []), ...wakes.map(setupNote).filter(Boolean)];
|
|
802
893
|
return { ok: true, post, ...(notes.length ? { note: notes.join(" ") } : {}) };
|
|
@@ -839,11 +930,10 @@ export async function startDaemon() {
|
|
|
839
930
|
notifyBoard(task);
|
|
840
931
|
return { ok: true, task };
|
|
841
932
|
}
|
|
933
|
+
// The same move as the composer's (swarm.mjs board.swarm.folder): between turns, each agent fresh in the new folder.
|
|
842
934
|
if (request.command === "board.goal.folder") {
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
store.setGoalFolder(goal.id, path.resolve(workspace));
|
|
846
|
-
return { ok: true, task: store.getTask(goal.id) };
|
|
935
|
+
const moved = swarmCommand({ command: "board.swarm.folder", id: goal.id, folder: String(request.workspace || "") }, "user");
|
|
936
|
+
return moved.ok ? { ok: true, task: store.getTask(goal.id) } : moved;
|
|
847
937
|
}
|
|
848
938
|
if (goal.status !== "paused") return { ok: false, error: `the goal is ${goal.status}` };
|
|
849
939
|
const task = store.setGoalState(goal.id, { status: "open", maxPosts: Number.isInteger(request.maxPosts) && request.maxPosts > 0 ? request.maxPosts : undefined });
|
|
@@ -865,10 +955,23 @@ export async function startDaemon() {
|
|
|
865
955
|
// "harness" (default): rounds go to Harness, Jev decides. "local": an agent on this machine decides with its tools off.
|
|
866
956
|
const coordinatorMode = () => (readJson(paths.config, {}).coordinator === "local" ? "local" : "harness");
|
|
867
957
|
const setCoordinatorMode = (mode) => writeJsonAtomic(paths.config, { ...readJson(paths.config, {}), coordinator: mode });
|
|
958
|
+
// The model each run starts with (models.mjs). Auto asks Harness, never on the privacy setting, and only a Harness that
|
|
959
|
+
// said it can pick models: one from before that would leave the run waiting for an answer that never comes.
|
|
960
|
+
// Each agent's own list of models, asked at start, once a day and when the person asks (models.mjs).
|
|
961
|
+
const listModels = () => refreshCatalogs(store, installedAgents.filter((agent) => PICKABLE.has(agent)), log);
|
|
962
|
+
// What Auto reads: the work itself, not the brief's instructions; a thread's turn is its request and its recent posts.
|
|
963
|
+
const workOf = (task) => {
|
|
964
|
+
if (task.kind !== "wake") return { work: `${task.objective}\n\n${task.brief}`.trim(), transcript: [] };
|
|
965
|
+
const thread = store.getTask(task.parent_id);
|
|
966
|
+
return { work: `${thread.objective}${thread.brief ? `\n\n${thread.brief}` : ""}`,
|
|
967
|
+
transcript: store.roomPosts(thread.id, 0, 500).filter((post) => !["note", "held", "round"].includes(post.kind)).slice(-30).map((post) => ({ author: post.author, text: post.text.slice(0, 20_000) })) };
|
|
968
|
+
};
|
|
969
|
+
const modelForRun = createModelRouter({ store, askHarness, canAsk: () => state.connected && serverFeatures.has("route_model"), privacy: () => coordinatorMode() === "local", ownModel: (agent) => modelOf(agent), workOf, log });
|
|
868
970
|
const coordinator = createCoordinator({
|
|
869
|
-
store, mode: coordinatorMode, askHarness, connected: () => state.connected, localAgents: () => installedAgents.filter((agent) => RUNNERS.has(agent)),
|
|
870
|
-
|
|
871
|
-
|
|
971
|
+
store, mode: coordinatorMode, askHarness, connected: () => state.connected, serverHas: (feature) => serverFeatures.has(feature), localAgents: () => installedAgents.filter((agent) => RUNNERS.has(agent)),
|
|
972
|
+
// Harness is one of them (owner, October 14): handed work, its open window or Harness's server takes it (offerToServer).
|
|
973
|
+
roster: () => cardsFor(available().filter((agent) => setupOf(agent)?.state !== "not_set_up")),
|
|
974
|
+
handOn: (goalId, agent, reason, { review, words }) => (RUNNERS.has(agent) ? wakeAgent(goalId, agent, reason, { review, go: personsGo("harness", agent) }) : askAgent(goalId, agent, reason, "harness", words)),
|
|
872
975
|
pauseGoal, notifyBoard, searchContext, log,
|
|
873
976
|
});
|
|
874
977
|
const swarmCommand = createSwarm({
|
|
@@ -877,7 +980,10 @@ export async function startDaemon() {
|
|
|
877
980
|
coordinator: { mode: coordinatorMode, setMode: setCoordinatorMode, state: (id) => coordinator.state(id) },
|
|
878
981
|
roster: () => available().map((id) => ({ id, about: aboutOf(id), setup: setupOf(id) })),
|
|
879
982
|
worktrees,
|
|
983
|
+
folders: createFolders({ store, askHarness, canAsk: () => state.connected && serverFeatures.has("pick_folder"), privacy: () => coordinatorMode() === "local", log }),
|
|
984
|
+
runsIn,
|
|
880
985
|
authorizeRun: (id) => boardCommand({ command: "board.run", agent: "user", id }),
|
|
986
|
+
pickable: () => installedAgents.filter((agent) => PICKABLE.has(agent)), listModels,
|
|
881
987
|
});
|
|
882
988
|
|
|
883
989
|
// The person's own sessions running now (live.mjs), and what they sent one from Harness: it waits here until that
|
|
@@ -927,6 +1033,8 @@ export async function startDaemon() {
|
|
|
927
1033
|
let env = placeEnv(request.env);
|
|
928
1034
|
let host = pids.map((pid) => hostApp(table, pid)).find(Boolean) ?? null;
|
|
929
1035
|
let tty = pids.map((pid) => table.get(pid)?.tty).find(Boolean) ?? null;
|
|
1036
|
+
// The agent's own process (not the shell a hook ran in), kept with the place: the session runs while it does.
|
|
1037
|
+
let agentPid = pids.find((pid) => path.basename(table.get(pid).comm) === PROCESS_NAMES[agentId]) ?? null;
|
|
930
1038
|
if (agentId === "codex" && pids.length && !host && !tty) {
|
|
931
1039
|
const clients = [...table].filter(([, row]) => path.basename(row.comm) === "codex" && row.tty).map(([pid]) => pid);
|
|
932
1040
|
const cwds = workingDirectories(clients);
|
|
@@ -935,8 +1043,11 @@ export async function startDaemon() {
|
|
|
935
1043
|
env = processEnv(client);
|
|
936
1044
|
host = hostApp(table, client);
|
|
937
1045
|
tty = table.get(client).tty;
|
|
1046
|
+
// Codex's shared server outlives its sessions; the terminal's Codex is this one.
|
|
1047
|
+
agentPid = client;
|
|
938
1048
|
}
|
|
939
|
-
|
|
1049
|
+
const place = placeFrom({ env, host, tty, folder });
|
|
1050
|
+
return place && agentPid ? { ...place, pid: agentPid, comm: table.get(agentPid).comm } : place;
|
|
940
1051
|
};
|
|
941
1052
|
// A custom harness may say how one of its sessions opens again: a page it serves on this computer, or a command to type.
|
|
942
1053
|
const reopenOf = (value) => {
|
|
@@ -956,7 +1067,10 @@ export async function startDaemon() {
|
|
|
956
1067
|
};
|
|
957
1068
|
const linksOk = t3LinksOk();
|
|
958
1069
|
const handed = handedToT3();
|
|
959
|
-
|
|
1070
|
+
// The sessions that named their own process when they started (pi's extension, OpenCode's plugin, hooks).
|
|
1071
|
+
const reported = LIVE_AGENTS.flatMap((agentId) => Object.entries(placesOf(agentId)).filter(([, place]) => Number.isInteger(place.pid))
|
|
1072
|
+
.map(([sessionId, place]) => ({ agent: agentId, sessionId, folder: place.folder || null, pid: place.pid, comm: place.comm, at: place.at })));
|
|
1073
|
+
const sessions = liveSessions({ exclude: new Set(runningPids()), recent, reported })
|
|
960
1074
|
// A T3 thread running a turn Harness handed it is Harness's run, shown in its thread.
|
|
961
1075
|
.filter((session) => !handed.has(session.t3Thread))
|
|
962
1076
|
// A T3 Code that cannot open links is brought forward instead, and messages reach its sessions through their hooks.
|
|
@@ -1022,10 +1136,14 @@ export async function startDaemon() {
|
|
|
1022
1136
|
const ownSession = (row) => !String(row.cwd || "").startsWith(paths.home + path.sep);
|
|
1023
1137
|
const short = (name) => String(name || "").replace(/ \(.*\)$/, "");
|
|
1024
1138
|
|
|
1139
|
+
/** Whether one of Harness's runs is still working in this session: it is not resumed beside it. */
|
|
1140
|
+
const runBusy = (sessionId) => Boolean(sessionId) && [...activeRuns.values()].some(({ id }) => store.getTask(id)?.session_id === sessionId);
|
|
1025
1141
|
/** How a session opens in one place (or the agent itself, with no session); null when that place cannot show it. */
|
|
1026
1142
|
const placePlan = (agentId, place, sessionId, folder, act) => {
|
|
1027
1143
|
if (!place) return null;
|
|
1028
1144
|
const name = short(place.name);
|
|
1145
|
+
const busy = runBusy(sessionId)
|
|
1146
|
+
? `${nameOf(agentId)} is still working on this${folder ? ` in ${path.basename(folder)}` : ""}; Open it again once it's done to carry on in the same session.` : null;
|
|
1029
1147
|
if (place.kind === "t3") {
|
|
1030
1148
|
if (!place.app) return null;
|
|
1031
1149
|
const thread = sessionId ? t3Thread(sessionId) : null;
|
|
@@ -1039,18 +1157,30 @@ export async function startDaemon() {
|
|
|
1039
1157
|
: "T3 Code doesn't list this session, so it is in front as it is." };
|
|
1040
1158
|
}
|
|
1041
1159
|
if (place.kind === "editor") {
|
|
1042
|
-
// Its window
|
|
1160
|
+
// Its window (the folder the editor has open around the session's), first when a panel link is to land there.
|
|
1043
1161
|
const window = editorFolder(place.editor, folder);
|
|
1044
|
-
|
|
1162
|
+
// Not in its panel or a terminal beside a run still working in it: its folder comes up, and the buddy shows the work.
|
|
1163
|
+
if (busy && place.app) { if (act && window) openEditorFolder(place, window); return { opensIn: name, app: place.app, note: busy }; }
|
|
1045
1164
|
const link = sessionId ? panelLink(agentId, place.editor, sessionId) : null;
|
|
1046
|
-
if (link) return { opensIn: name, link };
|
|
1165
|
+
if (link) { if (act && window) openEditorFolder(place, window); return { opensIn: name, link }; }
|
|
1047
1166
|
if (!place.app) return null;
|
|
1048
|
-
// An editor's terminal takes no command from another app: the session's own command is copied to paste there.
|
|
1049
1167
|
const line = sessionId ? resumeLine(agentId, sessionId, folder) : null;
|
|
1168
|
+
// The session itself, resumed in the editor's own terminal in a window on its folder: a workspace whose task runs
|
|
1169
|
+
// when the window opens (the editor asks once). The command is copied too, should the person not allow it.
|
|
1170
|
+
if (line && editorRunsOpenTasks(place.editor)) {
|
|
1171
|
+
if (act) openEditorFolder(place, editorSessionWorkspace(agentId, sessionId, folder, window));
|
|
1172
|
+
return { opensIn: name, app: place.app, copy: line, note: `${name} opens ${path.basename(window || folder)} with this session in its terminal. The first time, it asks you to trust the folder and to allow the task that resumes it; the command is copied too.` };
|
|
1173
|
+
}
|
|
1174
|
+
// Automatic tasks turned off there: the session's own command is copied to paste in its terminal.
|
|
1175
|
+
if (act && window) openEditorFolder(place, window);
|
|
1050
1176
|
return { opensIn: name, app: place.app, ...(line ? { copy: line, note: `${name} is up${window ? ` on ${path.basename(window)}` : ""}. The command that resumes this session is copied: paste it in a terminal there.` } : {}) };
|
|
1051
1177
|
}
|
|
1052
1178
|
if (place.kind === "codex-app") return agentId === "codex" && sessionId ? { opensIn: "Codex", link: `codex://threads/${sessionId}` } : place.app ? { opensIn: "Codex", app: place.app } : null;
|
|
1053
|
-
if (place.kind === "terminal")
|
|
1179
|
+
if (place.kind === "terminal") {
|
|
1180
|
+
if (!CODING_AGENTS.includes(agentId)) return null;
|
|
1181
|
+
if (busy) return place.app ? { opensIn: name, app: place.app, note: busy } : { opensIn: name, note: busy };
|
|
1182
|
+
return { opensIn: name, resume: { agent: agentId, session: sessionId ?? null, cwd: folder ?? null, app: place.app ?? null } };
|
|
1183
|
+
}
|
|
1054
1184
|
return place.app ? { opensIn: name, app: place.app } : null;
|
|
1055
1185
|
};
|
|
1056
1186
|
/** A running session, where it runs: its T3 thread, its panel, its editor's window, its terminal pane and tab. */
|
|
@@ -1126,7 +1256,8 @@ export async function startDaemon() {
|
|
|
1126
1256
|
// A session's latest messages and steps, from its own file (running or not).
|
|
1127
1257
|
if (request.command === "board.live.read") {
|
|
1128
1258
|
const agentId = String(request.to || "");
|
|
1129
|
-
if (!LIVE_AGENTS.includes(agentId)
|
|
1259
|
+
if (!LIVE_AGENTS.includes(agentId)) return { ok: false, error: "only a coding agent's session can be read here" };
|
|
1260
|
+
if (agentId === "opencode") return { ok: true, items: opencodeFeed(opencodeExecutable || "opencode", sessionId, Math.min(200, Number(request.limit) || 80)) };
|
|
1130
1261
|
const file = sessionFile(agentId, sessionId, folderOf(request), store.recentSessions(200));
|
|
1131
1262
|
if (!file) return { ok: true, items: [] };
|
|
1132
1263
|
return { ok: true, items: sessionFeed(agentId, file, Math.min(200, Number(request.limit) || 80)) };
|
|
@@ -1163,6 +1294,8 @@ export async function startDaemon() {
|
|
|
1163
1294
|
// Lid-Closed Mode's Standby: when anything last came in (a phone message, a wake, a new task).
|
|
1164
1295
|
let listenerAt = Date.now();
|
|
1165
1296
|
const touchListener = () => { listenerAt = Date.now(); };
|
|
1297
|
+
// Swarms talking to swarms (network.mjs): set once the relay's helpers below exist.
|
|
1298
|
+
let network = null;
|
|
1166
1299
|
const boardCommand = (request) => {
|
|
1167
1300
|
if (!validAgent(request.agent)) return { ok: false, error: "board requests need an agent name" };
|
|
1168
1301
|
if (request.command === "board.post") touchListener();
|
|
@@ -1174,6 +1307,7 @@ export async function startDaemon() {
|
|
|
1174
1307
|
let outcome;
|
|
1175
1308
|
if (String(request.command).startsWith("board.goal.") || String(request.command).startsWith("board.room.")) return goalCommand(request, agent);
|
|
1176
1309
|
if (String(request.command).startsWith("board.swarm.")) return swarmCommand(request, agent);
|
|
1310
|
+
if (String(request.command).startsWith("board.relay.")) return network ? network.command(request, agent) : { ok: false, error: "the daemon is still starting" };
|
|
1177
1311
|
if (request.command === "board.prompted") {
|
|
1178
1312
|
for (const pid of Array.isArray(request.pids) ? request.pids.slice(0, 2) : []) if (Number.isInteger(pid) && pid > 1) promptedBy.set(pid, agent);
|
|
1179
1313
|
// A session that started before its hook said where it runs says so on its next prompt.
|
|
@@ -1192,9 +1326,12 @@ export async function startDaemon() {
|
|
|
1192
1326
|
if (request.command === "board.post") {
|
|
1193
1327
|
const objective = String(request.objective || "").trim();
|
|
1194
1328
|
if (!objective) return { ok: false, error: "a task needs an objective" };
|
|
1329
|
+
// A thread with another person themselves (not one of their agents) is the person's own: no agent here claims or runs it.
|
|
1330
|
+
const personThread = Boolean(request.thread) && request.targetAgent === "person";
|
|
1331
|
+
if (personThread) request = { ...request, targetAgent: "user" };
|
|
1195
1332
|
// A target is named the way people write it ("Grok", "Claude Code") and resolved through the roster; a name
|
|
1196
1333
|
// several members start ("claude") or none matches is refused with who it could be, never guessed.
|
|
1197
|
-
if (request.targetAgent != null && !String(request.targetAgent).includes("/")) {
|
|
1334
|
+
if (request.targetAgent != null && !personThread && !String(request.targetAgent).includes("/")) {
|
|
1198
1335
|
const named = resolveMember(request.targetAgent, { extra: store.agentsSeen().map((row) => row.agent).filter((seen) => !member(seen)) });
|
|
1199
1336
|
if (!named.ok) {
|
|
1200
1337
|
return { ok: false, code: "unknown_agent", candidates: named.candidates, error: named.candidates.length
|
|
@@ -1205,6 +1342,14 @@ export async function startDaemon() {
|
|
|
1205
1342
|
request = { ...request, targetAgent: named.id };
|
|
1206
1343
|
}
|
|
1207
1344
|
if (request.targetAgent != null && !validAgent(request.targetAgent)) return { ok: false, error: "invalid target agent" };
|
|
1345
|
+
// The model and effort the poster names for the run, checked against the agent's own list (models.mjs).
|
|
1346
|
+
let pick = null;
|
|
1347
|
+
if (request.model) {
|
|
1348
|
+
if (!PICKABLE.has(String(request.targetAgent || ""))) return { ok: false, error: "a model can be named only for one of claude-code, codex, pi or opencode, as target_agent" };
|
|
1349
|
+
const resolved = resolvePick(store, String(request.targetAgent), request.model, request.effort);
|
|
1350
|
+
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
1351
|
+
pick = resolved.pick;
|
|
1352
|
+
}
|
|
1208
1353
|
// Provenance is code's: the local parent's label and what Harness sent from above the poster can only tighten it.
|
|
1209
1354
|
const parentTask = request.parentId ? store.getTask(String(request.parentId)) : null;
|
|
1210
1355
|
const claimed = request.provenance && typeof request.provenance === "object" && request.provenance.trust === "agent" ? request.provenance : null;
|
|
@@ -1226,8 +1371,8 @@ export async function startDaemon() {
|
|
|
1226
1371
|
return forwardToHarness({ ...request, provenance: upstream });
|
|
1227
1372
|
}
|
|
1228
1373
|
// Left out, a task from a coding agent for a coding agent runs in the folder the poster works in (its stdio MCP's,
|
|
1229
|
-
// which
|
|
1230
|
-
const posterFolder = !request.thread && !goalParent && request.inSession !== true &&
|
|
1374
|
+
// which each agent starts in the session's folder); never home or above it.
|
|
1375
|
+
const posterFolder = !request.thread && !goalParent && request.inSession !== true && RUNNERS.has(agent) && RUNNERS.has(String(request.targetAgent || ""))
|
|
1231
1376
|
&& typeof request.posterCwd === "string" && path.isAbsolute(request.posterCwd) && !aboveHome(request.posterCwd) ? request.posterCwd : null;
|
|
1232
1377
|
// The person's own task for a coding agent that names no folder runs in that agent's scratch folder, so it starts like
|
|
1233
1378
|
// any covered run (same daily limit, same off switch) instead of waiting for a session to look at the board.
|
|
@@ -1268,14 +1413,18 @@ export async function startDaemon() {
|
|
|
1268
1413
|
originAgent: agent, provenance, targetAgent: request.targetAgent ? String(request.targetAgent) : null, parentId: request.parentId ? String(request.parentId) : null, contextRefs: refs,
|
|
1269
1414
|
// Asking for a run in a folder is the person's Start; a scratch folder they never asked for goes by standing permission.
|
|
1270
1415
|
runWorkspace: thread ? null : runWorkspace, runAuthorizedBy: !thread && runWorkspace && !scratch && agent === "user" ? "user" : null });
|
|
1271
|
-
if (thread) store.openThread(task.id, { withAgent: String(thread.with), role: thread.role, peerKey: thread[OPENED] && thread.peerKey ? String(thread.peerKey) : null });
|
|
1272
|
-
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1416
|
+
if (thread) { store.openThread(task.id, { withAgent: String(thread.with), role: thread.role, peerKey: thread[OPENED] && thread.peerKey ? String(thread.peerKey) : null }); network?.opened(task.id); }
|
|
1417
|
+
if (pick) setRunPick(store, task.id, pick, agent);
|
|
1418
|
+
// A job in a goal's room runs on the goal's authority; anywhere else, standing permission decides. The goal covers
|
|
1419
|
+
// jobs from the person, Harness and the coding agents on this machine, with the person's go (personsGo) unless an
|
|
1420
|
+
// agent's words came from above the poster; a job from a member that runs elsewhere (Grok Bot, Muse, a connected
|
|
1421
|
+
// app) waits for the person's Start.
|
|
1275
1422
|
const onGoal = agent === "user" || agent === "harness" || (!request.hostedCaller && member(agent)?.runs === "machine");
|
|
1276
1423
|
const agentWritten = task.trust !== "user";
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1424
|
+
const go = onGoal && (!agentWritten || (!claimed && !setToWait(String(task.target_agent))));
|
|
1425
|
+
const mayStart = goalParent && task.run_workspace && threadMayRun(goalParent) && (go || (onGoal && restrictable(task.target_agent, task.run_workspace)));
|
|
1426
|
+
const startOnGoal = () => { if (go) store.meta(`person-wake:${task.id}`, "1"); return store.authorizeGoalRun(task.id); };
|
|
1427
|
+
const standing = thread ? undefined : goalParent ? (!task.run_workspace ? undefined : mayStart && startOnGoal() ? { started: true, by: "goal", ...(go ? {} : { restricted: true }) }
|
|
1279
1428
|
: !threadMayRun(goalParent) ? undefined
|
|
1280
1429
|
: { started: false, reason: !onGoal ? `${nameOf(agent)} does not run on this machine, so its job waits for the person's Start.` : restrictedWait(String(task.target_agent)) }) : applyStanding(task);
|
|
1281
1430
|
if (!thread && !goalParent && !request.parentId && task.target_agent && task.target_agent !== "harness" && ROOM_AGENTS.includes(task.target_agent)) openTaskRoom(store.getTask(task.id), agent, provenance, { hosted: Boolean(request.hostedCaller) });
|
|
@@ -1402,7 +1551,8 @@ export async function startDaemon() {
|
|
|
1402
1551
|
outcome = store.claimTask(id, agent, expected, Number.isInteger(request.leaseMinutes) ? request.leaseMinutes : 60);
|
|
1403
1552
|
// The person's own session that took it, so the task opens there (its MCP server is a child of that session).
|
|
1404
1553
|
const claimed = outcome?.ok ? store.getTask(id) : null;
|
|
1405
|
-
const
|
|
1554
|
+
const reportedBy = (pid) => Object.entries(placesOf(agent)).find(([, place]) => place.pid === pid)?.[0] ?? null;
|
|
1555
|
+
const session = claimed && !claimed.session_id && LIVE_AGENTS.includes(agent) ? sessionOfProcess(agent, request.claimerPid, store.recentSessions(200)) ?? reportedBy(request.claimerPid) : null;
|
|
1406
1556
|
if (session) { store.setRunSession(id, session); outcome = { ...outcome, task: store.getTask(id) }; }
|
|
1407
1557
|
}
|
|
1408
1558
|
else if (request.command === "board.report") outcome = store.reportTask(id, agent, { status: String(request.status || ""), result: String(request.result || "").slice(0, 200_000) }, expected);
|
|
@@ -1455,6 +1605,8 @@ export async function startDaemon() {
|
|
|
1455
1605
|
// One run per folder at a time: each thread's worktree is its own folder, so threads run side by side.
|
|
1456
1606
|
for (const task of store.runnableTasks(installedAgents)) {
|
|
1457
1607
|
const agent = task.target_agent;
|
|
1608
|
+
// Where a task runs is settled once, before its slot: a run T3 carries out holds its thread, not a folder.
|
|
1609
|
+
if (task.kind === "task" && store.meta(`t3-run:${task.id}`) === null) store.meta(`t3-run:${task.id}`, taskInT3(task, agent) ? "1" : "0");
|
|
1458
1610
|
const slot = slotOf(task);
|
|
1459
1611
|
if (activeRuns.has(slot)) continue;
|
|
1460
1612
|
const claimed = store.claimTask(task.id, agent, task.version, null);
|
|
@@ -1490,10 +1642,15 @@ export async function startDaemon() {
|
|
|
1490
1642
|
const resumeSession = wake ? store.goal(task.parent_id)?.sessions[agent] : stoppedFor ? task.session_id || undefined : undefined;
|
|
1491
1643
|
const again = stoppedFor ? `This task was stopped partway because ${stoppedFor}. Continue from where you left off; check the folder for what you already changed.` : "";
|
|
1492
1644
|
const readOnly = store.meta(`review:${task.id}`) === "1";
|
|
1493
|
-
// Another agent's words on a goal's authority,
|
|
1645
|
+
// Another agent's words on a goal's authority, without the person's tap or go (person-wake): restricted (docs/member-runs.md).
|
|
1494
1646
|
const restricted = task.trust !== "user" && task.run_authorized_by === "goal" && store.meta(`person-wake:${task.id}`) !== "1";
|
|
1495
|
-
|
|
1496
|
-
|
|
1647
|
+
// The model and effort it starts with (models.mjs), kept on the run with who chose them; without a choice, the model
|
|
1648
|
+
// the agent's own settings name, so each model's record covers every run.
|
|
1649
|
+
const chosen = await modelForRun(task, agent);
|
|
1650
|
+
const { model = null, effort = null } = chosen ?? {};
|
|
1651
|
+
store.setRunModel(task.id, { ...chosen, model: model ?? modelOf(agent), effort, by: model ? chosen.by : chosen?.by ?? "its settings", ...(model ? {} : { ownSettings: true }) });
|
|
1652
|
+
const t3 = store.meta(`t3-run:${task.id}`) === "1" ? t3Turn(task, agent, { model, effort }) : null;
|
|
1653
|
+
startTask({ runId: task.id, agent, title: task.objective.slice(0, 120), brief: resumeSession && again && !wake ? again : [again, brief].filter(Boolean).join("\n\n"), cwd: task.run_workspace, ...(model ? { model } : {}), ...(effort ? { effort } : {}), ...(stoppedFor ? { again: true } : {}), ...(resumeSession && !t3 ? { resumeSession } : {}), ...(readOnly ? { readOnly } : {}), ...(restricted ? { restricted } : {}), ...(t3 ? { t3 } : {}) }, (kind, text, extra) => {
|
|
1497
1654
|
// The agent's session in a thread is resumed when the thread calls on it again; in T3 it is the agent's T3 thread.
|
|
1498
1655
|
if (kind === "session" && text) { store.setRunSession(task.id, text); if (!t3 && (wake || room?.kind === "goal")) store.setGoalSession(task.parent_id, agent, text); notifyBoard(store.getTask(task.id)); }
|
|
1499
1656
|
// A step the agent took: whoever has the thread open reads it again, at most once a second per run.
|
|
@@ -1603,6 +1760,7 @@ export async function startDaemon() {
|
|
|
1603
1760
|
const opened = await openBound(await relayKey(relayIdentity.privateKey, peer), `request|${sealed.sealId}`, sealed);
|
|
1604
1761
|
return { about: String(opened.about || "").slice(0, 500), fingerprint: await fingerprint(peer) };
|
|
1605
1762
|
};
|
|
1763
|
+
network = createNetwork({ store, askHarness, relayRequest, openRelayRequest, fingerprint, myKey: () => relayIdentity?.publicKey ?? null });
|
|
1606
1764
|
|
|
1607
1765
|
// Once a browser is paired (store.sealedOnly), Harness's plain calls get metadata only: who, which state,
|
|
1608
1766
|
// when, the same as every board_changed already carries, and never the words (objectives, briefs,
|
|
@@ -1894,6 +2052,7 @@ export async function startDaemon() {
|
|
|
1894
2052
|
offlineSince = null;
|
|
1895
2053
|
state.connected = true;
|
|
1896
2054
|
state.lastError = null;
|
|
2055
|
+
serverFeatures = new Set();
|
|
1897
2056
|
saveStatus();
|
|
1898
2057
|
send({ type: "hello", protocolVersion: PROTOCOL_VERSION, daemonVersion: DAEMON_VERSION, agents: installedAgents, health: state.agents, capabilities: ["sessions.search", "sessions.read", "tasks.status", "board", "threads", "sealed", "sealed_threads", "sealed_files"], identityKey: relayIdentity?.publicKey });
|
|
1899
2058
|
// Threads that went quiet while Harness was out of reach get their round now.
|
|
@@ -1908,6 +2067,7 @@ export async function startDaemon() {
|
|
|
1908
2067
|
return;
|
|
1909
2068
|
}
|
|
1910
2069
|
if (message.type === "sync") { void sync(); return; }
|
|
2070
|
+
if (message.type === "server_features" && Array.isArray(message.features)) { serverFeatures = new Set(message.features.map(String)); return; }
|
|
1911
2071
|
if (message.type === "agent_setup" && Array.isArray(message.agents)) {
|
|
1912
2072
|
cloudSetup = new Map(message.agents.filter((entry) => entry && typeof entry.agent === "string" && SETUP_STATES.includes(entry.state))
|
|
1913
2073
|
.map((entry) => [entry.agent, { state: entry.state, how: typeof entry.how === "string" ? entry.how : null, detail: typeof entry.detail === "string" ? entry.detail : null }]));
|
|
@@ -1979,7 +2139,7 @@ export async function startDaemon() {
|
|
|
1979
2139
|
store.roomPost({ goalId: guestGoal, author: String(input.from || "guest"), kind: "guest", text: words.text.slice(0, 20_000) });
|
|
1980
2140
|
notifyBoard(store.getTask(guestGoal));
|
|
1981
2141
|
}
|
|
1982
|
-
if (notificationsOn()) notifyDesktop(`${input.from} replied on: ${String(task?.objective || "").slice(0, 80)}`);
|
|
2142
|
+
if (notificationsOn()) notifyDesktop(String(input.from || "").endsWith("/person") ? `${handleOf(input.from)} wrote to you: ${words.text.slice(0, 100)}` : `${input.from} replied on: ${String(task?.objective || "").slice(0, 80)}`);
|
|
1983
2143
|
}
|
|
1984
2144
|
})().catch((error) => send({ type: "bridge_response", requestId: message.requestId, ok: false, error: error.message }));
|
|
1985
2145
|
return;
|
|
@@ -2000,6 +2160,12 @@ export async function startDaemon() {
|
|
|
2000
2160
|
return;
|
|
2001
2161
|
}
|
|
2002
2162
|
if (message.type === "thread_ack") { store.markDelivered(String(message.messageId || "")); return; }
|
|
2163
|
+
// Someone asked to reach this person's swarm (network.mjs lists it): told on the desktop, answered in Swarms.
|
|
2164
|
+
if (message.type === "relay_requested") {
|
|
2165
|
+
const input = message.input || {};
|
|
2166
|
+
if (notificationsOn() && validAgent(input.from)) notifyDesktop(`${input.from} asked to reach ${input.to === "person" ? "you" : `your ${nameOf(String(input.to || ""))}`}. Answer it in Swarms.`);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2003
2169
|
if (message.type === "bridge_response" && relayWaiters.has(message.requestId)) {
|
|
2004
2170
|
relayWaiters.get(message.requestId)(message.ok ? { ok: true, ...(message.result || {}) } : { ok: false, error: message.error || "Harness refused the request" });
|
|
2005
2171
|
relayWaiters.delete(message.requestId);
|
|
@@ -2092,12 +2258,15 @@ export async function startDaemon() {
|
|
|
2092
2258
|
}
|
|
2093
2259
|
}
|
|
2094
2260
|
setImmediate(() => void sync());
|
|
2261
|
+
setImmediate(() => void listModels());
|
|
2262
|
+
const relist = setInterval(() => void listModels(), 24 * 60 * 60_000);
|
|
2095
2263
|
|
|
2096
2264
|
const shutdown = () => {
|
|
2097
2265
|
clearInterval(heartbeat);
|
|
2098
2266
|
clearInterval(staleCheck);
|
|
2099
2267
|
clearInterval(embedTimer);
|
|
2100
2268
|
clearInterval(rescan);
|
|
2269
|
+
clearInterval(relist);
|
|
2101
2270
|
for (const watcher of watchers) watcher.close();
|
|
2102
2271
|
try { socket?.close(); } catch {}
|
|
2103
2272
|
try { opencodeProcess?.kill(); } catch {}
|