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/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
- const POSTERS_WITH_FOLDER = new Set(["claude-code", "codex"]);
468
- // A session the person typed in: its prompt hook names the agent process, the parent of the board MCP it posts through.
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 (!POSTERS_WITH_FOLDER.has(agent) || !Number.isInteger(pid) || promptedBy.get(pid) !== agent) return false;
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
- // Work another agent asked for runs on a goal's authority only where it can be held in (docs/member-runs.md): Claude
590
- // Code in restricted mode, in a copy of the folder this daemon made. Anything else waits for the person's Start.
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 restrictedWait = (agent) => agent === "claude-code"
594
- ? `${nameOf(agent)} would work in your own folder here, not a copy, so work another agent asked for waits for your Start.`
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
- if (tree && !(tree.state === "open" && fs.existsSync(tree.path))) return false;
612
- return t3Access()?.state === "send" && homeOf(agent)?.kind === "t3" && t3Open() && Boolean(t3Project(thread.goal.workspace)) && Boolean(t3Model(agent));
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
- const t3Turn = (task, agent) => {
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
- // byPerson: the person's own post named this agent, which is their go for this turn; any other wake (the coordinator,
633
- // another agent) is work another agent asked for and runs restricted or waits (docs/member-runs.md).
634
- const wakeAgent = (goalId, agent, reason, { review = false, byPerson = false } = {}) => {
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. Add one with: npx tryharness goal folder ${goalId} <dir>` });
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, byPerson: byPerson || Boolean(pendingWakes.get(`${goalId}:${agent}`)?.byPerson) }); return; }
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
- const wake = store.postTask({ objective: `Room: ${task.objective}`.slice(0, 500), brief, originAgent: "harness", provenance: { trust: "agent", chain: ["room"] }, targetAgent: agent, parentId: goalId, runWorkspace: folder, kind: "wake" });
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 (byPerson) { store.meta(`person-wake:${wake.id}`, "1"); store.authorizeGoalRun(wake.id); }
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
- const wakeFor = (goalId, post, author, names = post.mentions) => {
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, { byPerson: author === "user" });
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, { byPerson: pending.byPerson }); }
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 || ""}`.slice(0, 20_000),
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 workspace = String(request.workspace || "");
844
- if (!path.isAbsolute(workspace) || !fs.statSync(workspace, { throwIfNoEntry: false })?.isDirectory()) return { ok: false, error: `${workspace} is not an absolute path to a folder` };
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
- roster: () => cardsFor(available().filter((agent) => agent !== "harness" && setupOf(agent)?.state !== "not_set_up")),
871
- handOn: (goalId, agent, reason, { review }) => (RUNNERS.has(agent) ? wakeAgent(goalId, agent, reason, { review }) : askAgent(goalId, agent, reason, "harness")),
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
- return placeFrom({ env, host, tty, folder });
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
- const sessions = liveSessions({ exclude: new Set(runningPids()), recent })
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 first (the folder the editor has open around the session's), so a panel link lands there.
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
- if (act && window) openEditorFolder(place, window);
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") return CODING_AGENTS.includes(agentId) ? { opensIn: name, resume: { agent: agentId, session: sessionId ?? null, cwd: folder ?? null, app: place.app ?? null } } : null;
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) && agentId !== "pi") return { ok: false, error: "only a coding agent's session can be read here" };
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 Claude Code and Codex start in the session's folder); never home or above it.
1230
- const posterFolder = !request.thread && !goalParent && request.inSession !== true && POSTERS_WITH_FOLDER.has(agent) && RUNNERS.has(String(request.targetAgent || ""))
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
- // A job in a goal's room runs on the goal's authority; anywhere else, standing permission decides. Until runs
1273
- // started by members are restricted, the goal covers jobs from the person, Harness and the coding agents on this
1274
- // machine; a job from a member that runs elsewhere (Grok Bot, Muse, a connected app) waits for the person's Start.
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 mayStart = goalParent && task.run_workspace && threadMayRun(goalParent) && onGoal && (!agentWritten || restrictable(task.target_agent, task.run_workspace));
1278
- const standing = thread ? undefined : goalParent ? (!task.run_workspace ? undefined : mayStart && store.authorizeGoalRun(task.id) ? { started: true, by: "goal", ...(agentWritten ? { restricted: true } : {}) }
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 session = claimed && !claimed.session_id && LIVE_AGENTS.includes(agent) ? sessionOfProcess(agent, request.claimerPid, store.recentSessions(200)) : null;
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, not the person's tap: restricted (docs/member-runs.md).
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
- const t3 = wake && store.meta(`t3-run:${task.id}`) === "1" ? t3Turn(task, agent) : null;
1496
- 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, ...(stoppedFor ? { again: true } : {}), ...(resumeSession && !t3 ? { resumeSession } : {}), ...(readOnly ? { readOnly } : {}), ...(restricted ? { restricted } : {}), ...(t3 ? { t3 } : {}) }, (kind, text, extra) => {
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 {}