tryharness 0.7.8 → 0.7.9

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/cli.mjs CHANGED
@@ -649,8 +649,33 @@ async function link({ quiet = false } = {}) {
649
649
  }
650
650
  }
651
651
 
652
+ // Approving Harness in T3 Code (lib/t3.mjs): then what you send your T3 Code sessions from Harness, in the desktop app or
653
+ // on your phone, goes straight into their threads. No T3 account; T3's page asks for a pairing code from its settings.
654
+ const t3Words = (access) => !access ? "Harness is not approved in T3 Code. Messages to its sessions wait for them to pick them up (run: tryharness t3)."
655
+ : access.state === "send" ? `Harness sends your messages into T3 Code's threads, until ${access.until.slice(0, 10)}.`
656
+ : access.state === "read" ? "Harness was approved in T3 Code to read only, so messages still wait for its sessions to pick them up. Approve it again with the access your threads run with: tryharness t3"
657
+ : "Harness's approval in T3 Code ran out. Approve it again: tryharness t3";
658
+ async function t3() {
659
+ const sub = argv[0] || "connect";
660
+ if (sub === "status") { console.log(t3Words((await daemonRequest({ command: "t3.status" })).t3)); return; }
661
+ if (sub === "forget") { await daemonRequest({ command: "t3.forget" }); console.log("Harness forgot its approval. To end it in T3 Code as well: Settings → Connections."); return; }
662
+ if (sub !== "connect") throw new Error("usage: tryharness t3 [connect [--print] | status | forget]");
663
+ const before = (await daemonRequest({ command: "t3.status" })).t3;
664
+ const started = await daemonRequest({ command: "t3.connect" }, 20_000);
665
+ if (!started.ok) throw new Error(started.error || "the daemon could not start the approval");
666
+ const opened = !has("--print") && openUrl(started.url);
667
+ console.log(opened ? "T3 Code's approval page is open in your browser." : `Open this in a browser on this computer:\n\n${started.url}\n`);
668
+ console.log("There, enter a pairing code from T3 Code (Settings → Connections), then choose what Harness may do. To message threads that run in Full access, choose Full access.");
669
+ console.log("Waiting for your approval (Ctrl-C stops waiting; an approval still counts).");
670
+ for (;;) {
671
+ await sleep(1_000);
672
+ const now = (await daemonRequest({ command: "t3.status" })).t3;
673
+ if (now && JSON.stringify(now) !== JSON.stringify(before)) { console.log(t3Words(now)); return; }
674
+ }
675
+ }
676
+
652
677
  function help() {
653
- console.log(`tryharness ${VERSION}\n\nCommands:\n tryharness set up everything and start the daemon\n tryharness desktop open the Harness desktop app (setup opens it too; --no-desktop skips it)\n tryharness status show daemon, bridge, agent, and index health\n tryharness doctor diagnose the complete connection\n tryharness sync [--agent] rescan local coding history in the background\n tryharness task <id> --follow\n follow a delegated agent run in this terminal\n tryharness board [--all] list open work on the machine's task board\n tryharness board post --for <agent> --objective <text> [--file <brief.md>]\n tryharness board show <id>\n tryharness board post --for codex --objective <text> --run --workspace <dir>\n the daemon runs it and reports back on the task\n tryharness board run <id> authorize a run an agent asked for\n tryharness board release <id>\n tryharness board agents when each agent last read the board\n tryharness board --watch follow the board in this terminal\n tryharness notify on|off desktop notifications for what waits on you\n tryharness board send <task> <message> send a held reply to another person's agent\n tryharness board auto <task> on|off auto-send replies on that thread\n tryharness board end <task> --outcome <text>\n tryharness board context <task> <query> search everything written about a task\n tryharness goal set --objective <text> [--team a,b] [--workspace <dir>] a goal your agents work together in one room\n tryharness goal [list | show <id> | stop <id> | resume <id>]\n tryharness room <goal> | room post <goal> <text> [--ask agent,agent]\n tryharness swarm rounds [<thread>] what the coordinator saw, chose and cost each round\n tryharness swarm coordinator [harness|local] who coordinates your threads\n tryharness connect <handle/agent> --about <text> ask another person's agent to work an issue with yours, sealed to their computer\n tryharness peers the keys of other people's computers this one has worked with, to compare\n tryharness agents how Harness wakes each of your agents, and set up the rest\n tryharness agents setup <agent> | agents test <agent>\n tryharness mcp grok-bot print the MCP snippet that puts Grok Bot on the board\n tryharness link connect the browser you use Harness in to this computer, end to end (--print shows the link)\n tryharness phone pair show a QR code that pairs your phone with this computer\n tryharness phone list | revoke <id>\n tryharness logs [--follow]\n tryharness disconnect\n`);
678
+ console.log(`tryharness ${VERSION}\n\nCommands:\n tryharness set up everything and start the daemon\n tryharness desktop open the Harness desktop app (setup opens it too; --no-desktop skips it)\n tryharness status show daemon, bridge, agent, and index health\n tryharness doctor diagnose the complete connection\n tryharness sync [--agent] rescan local coding history in the background\n tryharness task <id> --follow\n follow a delegated agent run in this terminal\n tryharness board [--all] list open work on the machine's task board\n tryharness board post --for <agent> --objective <text> [--file <brief.md>]\n tryharness board show <id>\n tryharness board post --for codex --objective <text> --run --workspace <dir>\n the daemon runs it and reports back on the task\n tryharness board run <id> authorize a run an agent asked for\n tryharness board release <id>\n tryharness board agents when each agent last read the board\n tryharness board --watch follow the board in this terminal\n tryharness notify on|off desktop notifications for what waits on you\n tryharness board send <task> <message> send a held reply to another person's agent\n tryharness board auto <task> on|off auto-send replies on that thread\n tryharness board end <task> --outcome <text>\n tryharness board context <task> <query> search everything written about a task\n tryharness goal set --objective <text> [--team a,b] [--workspace <dir>] a goal your agents work together in one room\n tryharness goal [list | show <id> | stop <id> | resume <id>]\n tryharness room <goal> | room post <goal> <text> [--ask agent,agent]\n tryharness swarm rounds [<thread>] what the coordinator saw, chose and cost each round\n tryharness swarm coordinator [harness|local] who coordinates your threads\n tryharness connect <handle/agent> --about <text> ask another person's agent to work an issue with yours, sealed to their computer\n tryharness peers the keys of other people's computers this one has worked with, to compare\n tryharness agents how Harness wakes each of your agents, and set up the rest\n tryharness agents setup <agent> | agents test <agent>\n tryharness mcp grok-bot print the MCP snippet that puts Grok Bot on the board\n tryharness link connect the browser you use Harness in to this computer, end to end (--print shows the link)\n tryharness phone pair show a QR code that pairs your phone with this computer\n tryharness phone list | revoke <id>\n tryharness t3 approve Harness in T3 Code once, so your messages go into its threads (t3 status | t3 forget)\n tryharness logs [--follow]\n tryharness disconnect\n`);
654
679
  }
655
680
 
656
681
  try {
@@ -668,6 +693,7 @@ try {
668
693
  else if (command === "phone") await phone();
669
694
  else if (command === "agents") await agents();
670
695
  else if (command === "link") await link();
696
+ else if (command === "t3") await t3();
671
697
  else if (command === "peers") await peers();
672
698
  else if (command === "connect") await connectAgent();
673
699
  else if (command === "desktop") { if (!desktop()) process.exitCode = 1; }
package/lib/daemon.mjs CHANGED
@@ -30,6 +30,7 @@ import { machineActivity } from "./activity.mjs";
30
30
  import { LIVE_AGENTS, focusTerminal, hostApp, liveSessions, processTable, sessionFeed, sessionFile, sessionOfProcess, workingDirectories } from "./live.mjs";
31
31
  import { EDITOR_IDS, agentCommand, editorFolder, editorInstall, editorName, editorTerminalStarts, openEditorFolder, panelLink, placeEnv, placeFrom, placeKey, processEnv, resumeLine, t3App, t3Link, t3OpensLinks, t3Runs, t3Thread } from "./places.mjs";
32
32
  import { detectMcpClients } from "./adapters.mjs";
33
+ import { forgetT3, sendToT3, startT3SignIn, t3Access } from "./t3.mjs";
33
34
 
34
35
  // What may be referenced on a thread with another person (the server's rule, src/server/relay/refs.ts, kept
35
36
  // here since a sealed request is opened only on the machines).
@@ -1009,7 +1010,7 @@ export async function startDaemon() {
1009
1010
  if (!LIVE_AGENTS.includes(row.agent) || last[row.agent] || !ownSession(row)) continue;
1010
1011
  last[row.agent] = { sessionId: nativeSessionId(row.agent, row.session_id), folder: row.cwd || null };
1011
1012
  }
1012
- return { ok: true, sessions: liveNow(), last, homes: homes() };
1013
+ return { ok: true, sessions: liveNow(), last, homes: homes(), t3: t3Access() };
1013
1014
  }
1014
1015
  // A session's latest messages and steps, from its own file (running or not).
1015
1016
  if (request.command === "board.live.read") {
@@ -1031,9 +1032,19 @@ export async function startDaemon() {
1031
1032
  if (!text) return { ok: false, error: "a message needs text" };
1032
1033
  const session = liveNow().find((row) => row.agent === request.to && row.sessionId === sessionId);
1033
1034
  if (!session) return { ok: false, error: "that session is not running any more" };
1034
- const messages = [...said(session.agent, sessionId), { text, at: new Date().toISOString() }];
1035
- store.meta(saidKey(session.agent, sessionId), JSON.stringify(messages));
1036
- return { ok: true, session: { ...session, waiting: messages.length } };
1035
+ // Kept until the session's own hook takes it: when its turn ends, or the person next types there.
1036
+ const keep = (t3) => {
1037
+ const messages = [...said(session.agent, sessionId), { text, at: new Date().toISOString() }];
1038
+ store.meta(saidKey(session.agent, sessionId), JSON.stringify(messages));
1039
+ return { ok: true, sent: "hooks", session: { ...session, waiting: messages.length }, ...(t3 ? { t3 } : {}) };
1040
+ };
1041
+ // A session in T3 Code, once the person approved Harness there: into its T3 thread, as T3 sends any message.
1042
+ const thread = session.host && /^T3 Code\b/.test(session.host.name) ? t3Thread(sessionId) : null;
1043
+ if (!thread) return keep(null);
1044
+ const access = t3Access();
1045
+ if (access?.state !== "send") return keep(!access ? "not_connected" : access.state === "read" ? "read_only" : access.state);
1046
+ return { pending: sendToT3(thread, text).then((delivery) => ({ ok: true, sent: "t3", delivery, session }),
1047
+ (error) => { log(`T3 Code did not take a message: ${error.message}`); return keep(error.code || "failed"); }) };
1037
1048
  }
1038
1049
  return { ok: false, error: `unknown command ${request.command}` };
1039
1050
  };
@@ -1579,6 +1590,10 @@ export async function startDaemon() {
1579
1590
  else if (request.command === "phone.list") reply({ ok: true, phones: phones.list() });
1580
1591
  else if (request.command === "phone.revoke") reply(phones.revoke(request.id));
1581
1592
  else if (request.command === "wake.local") reply({ ok: true, agents: localWaker.status() });
1593
+ // Approving Harness in T3 Code: the page to approve it on, for the desktop app or the CLI to open.
1594
+ else if (request.command === "t3.connect") startT3SignIn().then((started) => reply({ ok: true, ...started }), (error) => reply({ ok: false, error: error.message }));
1595
+ else if (request.command === "t3.status") reply({ ok: true, t3: t3Access() });
1596
+ else if (request.command === "t3.forget") { forgetT3(); reply({ ok: true }); }
1582
1597
  // How Harness wakes the agents it keeps on the server (Grok Bot): read and saved there, never here.
1583
1598
  else if (request.command === "wake.server") askHarness("wake", request.input || {}).then(reply, (error) => reply({ ok: false, error: error.message }));
1584
1599
  // Whether agent work is going on here, for the desktop app deciding when the Mac may sleep.
package/lib/phones.mjs CHANGED
@@ -24,6 +24,8 @@ const PHONE_COMMANDS = new Set([
24
24
  "board.goal.list", "board.goal.get", "board.goal.stop", "board.room.read",
25
25
  "board.swarm.list", "board.swarm.read", "board.swarm.start", "board.swarm.post", "board.swarm.stop", "board.swarm.stop_all", "board.swarm.resume", "board.swarm.folder", "board.swarm.coordinator",
26
26
  "board.swarm.diff", "board.swarm.merge", "board.swarm.discard", "board.swarm.start_run",
27
+ // The person's own running sessions, read and messaged as from the desktop app; never queued (a session may be gone).
28
+ "board.live", "board.live.read", "board.live.say",
27
29
  ]);
28
30
  // What the chat owner does on the board, in Harness's name (was src/server/mcp/board.ts HARNESS_COMMANDS).
29
31
  const HARNESS_COMMANDS = new Set([
package/lib/t3.mjs ADDED
@@ -0,0 +1,172 @@
1
+ import http from "node:http";
2
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { readJson, writeJsonAtomic } from "./files.mjs";
6
+ import { paths } from "./paths.mjs";
7
+ import { t3Home } from "./places.mjs";
8
+
9
+ // T3 Code's door for agents it did not start (its /mcp; T3's docs/user/outside-agents.md). Harness signs in once and the
10
+ // person approves it inside T3, choosing what it may do; T3 asks for a pairing code from its Settings → Connections, and
11
+ // no T3 account. From then on a message the person sends a session running in T3 Code (from the desktop app or the
12
+ // phone) goes into that session's T3 thread as T3 sends any message: started when the thread is idle, queued behind its
13
+ // turn when it is busy. T3 shows it as sent by an agent through MCP. The key lives in ~/.harness/t3.json (0600), works
14
+ // only on T3's /mcp on this computer's loopback, lasts 30 days, and is revoked in T3 under Settings → Connections.
15
+
16
+ // T3's desktop app binds the first free port from 3773 up (T3 apps/server/src/auth/utils.ts).
17
+ const FIRST_PORT = 3773;
18
+ const PORTS = 20;
19
+ // Its descriptor answered in at most 1.7 ms over 40 tries while T3 ran four busy sessions (October 12); a closed port
20
+ // refuses in under 0.4 ms. The limit only keeps a port held by something else from stalling the search.
21
+ const PROBE_MS = 1_000;
22
+ const PROTOCOL = "2025-06-18";
23
+
24
+ const b64u = (bytes) => Buffer.from(bytes).toString("base64url");
25
+ const file = () => path.join(paths.home, "t3.json");
26
+
27
+ class T3Error extends Error {
28
+ constructor(code, message) { super(message); this.code = code; }
29
+ }
30
+
31
+ /** This computer's T3 Code: its environment (from T3's own data folder) and the loopback address that answers as it. */
32
+ export async function findT3(home = t3Home(), hint = null) {
33
+ let environmentId = "";
34
+ try { environmentId = fs.readFileSync(path.join(home, "environment-id"), "utf8").trim(); } catch { return null; }
35
+ const answers = async (origin) => {
36
+ try {
37
+ const response = await fetch(`${origin}/.well-known/t3/environment`, { signal: AbortSignal.timeout(PROBE_MS) });
38
+ return response.ok && (await response.json())?.environmentId === environmentId;
39
+ } catch { return false; }
40
+ };
41
+ if (hint && await answers(hint)) return { origin: hint, environmentId };
42
+ for (let port = FIRST_PORT; port < FIRST_PORT + PORTS; port++) {
43
+ const origin = `http://127.0.0.1:${port}`;
44
+ if (origin !== hint && await answers(origin)) return { origin, environmentId };
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /** What Harness may do in T3 Code: "send", "read", "expired", or null before the person approves it. */
50
+ export function t3Access() {
51
+ const saved = readJson(file(), null);
52
+ if (!saved?.token) return null;
53
+ if (!(Date.parse(saved.expiresAt) > Date.now())) return { state: "expired", at: saved.expiresAt };
54
+ return { state: String(saved.scope || "").split(" ").includes("orchestration:operate") ? "send" : "read", until: saved.expiresAt };
55
+ }
56
+
57
+ export function forgetT3() {
58
+ try { fs.rmSync(file()); } catch {}
59
+ }
60
+
61
+ const json = async (url, body) => {
62
+ const response = await fetch(url, body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {});
63
+ const value = await response.json().catch(() => null);
64
+ if (!response.ok || !value) throw new T3Error("t3", value?.error_description || `T3 Code answered ${response.status}`);
65
+ return value;
66
+ };
67
+
68
+ const page = (title, words) => `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>`
69
+ + `<body style="font-family:-apple-system,system-ui,sans-serif;display:grid;place-items:center;min-height:90vh;margin:0"><main style="max-width:420px;padding:24px"><h1 style="font-size:20px">${title}</h1><p style="line-height:1.5">${words}</p></main>`;
70
+
71
+ let pending = null;
72
+
73
+ /**
74
+ * Starts the sign-in: registers Harness with this computer's T3 Code and returns the page where the person approves it.
75
+ * The approval comes back to a loopback address here, which keeps listening until it does or another sign-in starts.
76
+ */
77
+ export async function startT3SignIn() {
78
+ const t3 = await findT3(t3Home(), readJson(file(), null)?.origin ?? null);
79
+ if (!t3) throw new T3Error("not_running", "T3 Code is not running on this computer. Open it, then try again.");
80
+ // T3 Code takes outside agents from its 0.0.46 builds on; an older one has no sign-in to offer.
81
+ const server = await json(`${t3.origin}/.well-known/oauth-authorization-server`).catch(() => {
82
+ throw new T3Error("too_old", "This T3 Code can't approve outside agents yet. Update it (0.0.46 or newer), then try again.");
83
+ });
84
+ pending?.close();
85
+ const listener = http.createServer();
86
+ pending = listener;
87
+ await new Promise((resolve, reject) => { listener.once("error", reject); listener.listen(0, "127.0.0.1", resolve); });
88
+ const redirect = `http://127.0.0.1:${listener.address().port}/t3/callback`;
89
+ const client = await json(server.registration_endpoint, { client_name: "Harness", redirect_uris: [redirect] });
90
+ const verifier = b64u(randomBytes(32));
91
+ const state = b64u(randomBytes(16));
92
+ const resource = `${t3.origin}/mcp`;
93
+ listener.on("request", async (request, response) => {
94
+ const url = new URL(request.url, redirect);
95
+ const answer = (status, title, words) => { response.writeHead(status, { "content-type": "text/html; charset=utf-8" }); response.end(page(title, words)); };
96
+ if (url.pathname !== "/t3/callback" || url.searchParams.get("state") !== state) return answer(400, "Not this sign-in", "This page is not the sign-in Harness started. Start it again from Harness.");
97
+ const done = () => { listener.close(); if (pending === listener) pending = null; };
98
+ if (url.searchParams.get("error")) { done(); return answer(200, "Harness was not approved", "Nothing changed. Harness keeps leaving your messages for T3 Code's sessions to pick up themselves."); }
99
+ if (url.searchParams.has("iss") && url.searchParams.get("iss") !== server.issuer) return answer(400, "Not this sign-in", "The approval came from another server.");
100
+ try {
101
+ const exchanged = await fetch(server.token_endpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" },
102
+ body: new URLSearchParams({ grant_type: "authorization_code", code: url.searchParams.get("code") || "", redirect_uri: redirect, client_id: client.client_id, code_verifier: verifier, resource }) });
103
+ const token = await exchanged.json().catch(() => null);
104
+ if (!exchanged.ok || !token?.access_token) throw new Error(token?.error_description || `T3 Code answered ${exchanged.status}`);
105
+ writeJsonAtomic(file(), { origin: t3.origin, environmentId: t3.environmentId, clientId: client.client_id, token: token.access_token, scope: token.scope || "",
106
+ expiresAt: new Date(Date.now() + Number(token.expires_in || 0) * 1000).toISOString(), approvedAt: new Date().toISOString() });
107
+ done();
108
+ const send = t3Access()?.state === "send";
109
+ answer(200, send ? "Harness can send into T3 Code" : "Harness can read T3 Code", send
110
+ ? "Messages you send your T3 Code sessions from Harness, on this computer or your phone, now go straight into their threads. You can close this tab."
111
+ : "You approved read only, so messages still wait for your sessions to pick them up. To send into T3 Code's threads, approve Harness again with the access your threads run with.");
112
+ } catch (error) {
113
+ answer(502, "The sign-in did not finish", `${String(error.message).replace(/[<>&]/g, "")}. Start it again from Harness.`);
114
+ }
115
+ });
116
+ const url = `${server.authorization_endpoint}?${new URLSearchParams({ response_type: "code", client_id: client.client_id, redirect_uri: redirect,
117
+ code_challenge: b64u(createHash("sha256").update(verifier).digest()), code_challenge_method: "S256", state, resource })}`;
118
+ return { url };
119
+ }
120
+
121
+ /** One JSON-RPC message to T3's /mcp (Streamable HTTP): the answer comes as JSON or as a server-sent event. */
122
+ async function rpc(origin, token, session, message) {
123
+ const response = await fetch(`${origin}/mcp`, { method: "POST", body: JSON.stringify({ jsonrpc: "2.0", ...message }), headers: {
124
+ authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json, text/event-stream", "mcp-protocol-version": PROTOCOL,
125
+ ...(session ? { "mcp-session-id": session } : {}) } });
126
+ if (response.status === 401 || response.status === 403) throw new T3Error("expired", "T3 Code no longer accepts Harness's sign-in.");
127
+ if (!response.ok) throw new T3Error("t3", `T3 Code answered ${response.status}`);
128
+ const next = response.headers.get("mcp-session-id") || session;
129
+ if (message.id === undefined) return { session: next };
130
+ const text = await response.text();
131
+ const replies = (response.headers.get("content-type") || "").includes("text/event-stream")
132
+ ? text.split("\n").filter((line) => line.startsWith("data:")).map((line) => { try { return JSON.parse(line.slice(5)); } catch { return null; } })
133
+ : [JSON.parse(text)];
134
+ const reply = replies.find((entry) => entry?.id === message.id);
135
+ if (!reply) throw new T3Error("t3", "T3 Code sent no answer");
136
+ if (reply.error) throw new T3Error("t3", reply.error.message || "T3 Code refused");
137
+ return { session: next, result: reply.result };
138
+ }
139
+
140
+ async function callTool(origin, token, name, args) {
141
+ let { session } = await rpc(origin, token, null, { id: 1, method: "initialize", params: { protocolVersion: PROTOCOL, capabilities: {}, clientInfo: { name: "Harness", version: "1" } } });
142
+ try {
143
+ ({ session } = await rpc(origin, token, session, { method: "notifications/initialized" }));
144
+ const { result } = await rpc(origin, token, session, { id: 2, method: "tools/call", params: { name, arguments: args } });
145
+ const words = (result?.content || []).map((part) => part?.text || "").join("\n").trim();
146
+ if (result?.isError) throw new T3Error("refused", words || "T3 Code refused");
147
+ if (result?.structuredContent) return result.structuredContent;
148
+ try { return JSON.parse(words); } catch { return {}; }
149
+ } finally {
150
+ if (session) fetch(`${origin}/mcp`, { method: "DELETE", headers: { authorization: `Bearer ${token}`, "mcp-session-id": session } }).catch(() => {});
151
+ }
152
+ }
153
+
154
+ /** Sends the person's words into a T3 Code thread: "started" or "queued" behind its turn. */
155
+ export async function sendToT3(threadId, text) {
156
+ const saved = readJson(file(), null);
157
+ const access = t3Access();
158
+ if (!access) throw new T3Error("not_connected", "Harness is not approved in T3 Code yet.");
159
+ if (access.state === "expired") throw new T3Error("expired", "Harness's approval in T3 Code ran out.");
160
+ if (access.state !== "send") throw new T3Error("read_only", "Harness was approved in T3 Code to read only.");
161
+ // T3 may come back on another port after a restart: the saved address first, else the search again.
162
+ const t3 = await findT3(t3Home(), saved.origin);
163
+ if (!t3 || t3.environmentId !== saved.environmentId) throw new T3Error("not_running", "T3 Code is not running on this computer.");
164
+ if (t3.origin !== saved.origin) writeJsonAtomic(file(), { ...saved, origin: t3.origin });
165
+ try {
166
+ const sent = await callTool(t3.origin, saved.token, "t3_thread_send", { threadId, message: text, mode: "queue", clientRequestId: randomUUID() });
167
+ return sent.delivery === "queued" ? "queued" : "started";
168
+ } catch (error) {
169
+ if (error.code === "expired") writeJsonAtomic(file(), { ...saved, expiresAt: new Date(0).toISOString() });
170
+ throw error;
171
+ }
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tryharness",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "Harness Relay: the daemon that turns the agents on this machine into your personal swarm. Memory, history, and work handed between any of them.",
5
5
  "bin": {
6
6
  "tryharness": "cli.mjs"
@@ -36,8 +36,8 @@
36
36
  "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c"
37
37
  },
38
38
  "optionalDependencies": {
39
- "tryharness-desktop-darwin-arm64": "0.7.8",
40
- "tryharness-desktop-darwin-x64": "0.7.8",
39
+ "tryharness-desktop-darwin-arm64": "0.7.9",
40
+ "tryharness-desktop-darwin-x64": "0.7.9",
41
41
  "tryharness-desktop-linux-x64": "0.7.4",
42
42
  "tryharness-desktop-win32-x64": "0.7.4"
43
43
  }