svamp-cli 0.2.113 → 0.2.115
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/dist/{agentCommands-zkS91i_Q.mjs → agentCommands-CFWM6S7e.mjs} +4 -4
- package/dist/{auth-DR24t5Ld.mjs → auth-B3NsDWG9.mjs} +2 -2
- package/dist/cli.mjs +76 -52
- package/dist/{commands-OvJA3HMg.mjs → commands-B3NhziMR.mjs} +56 -8
- package/dist/{commands--7ZWIjpI.mjs → commands-DbQ14J-R.mjs} +2 -2
- package/dist/{commands-DqFpATSJ.mjs → commands-Dj2M3sTB.mjs} +2 -2
- package/dist/{commands-KZSTNdxq.mjs → commands-Dmh59asw.mjs} +2 -2
- package/dist/{commands-DNEyZ3Vj.mjs → commands-Vudp6ihZ.mjs} +5 -5
- package/dist/{fleet-BGK4vxm9.mjs → fleet-F8KB5IcM.mjs} +1 -1
- package/dist/{frpc-fsq8Ua7l.mjs → frpc-1aSnPVrE.mjs} +2 -2
- package/dist/{headlessCli-B7ZcrWiW.mjs → headlessCli-6Cps9gnO.mjs} +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{package-CFN2KBIk.mjs → package-BuIQUz-N.mjs} +2 -2
- package/dist/{run-CZ8x2LxA.mjs → run-BnPtZvoP.mjs} +1 -1
- package/dist/{run-g8QqDNJ1.mjs → run-DXzaCfex.mjs} +678 -211
- package/dist/{serveCommands-BRx6Jc8n.mjs → serveCommands-CFO3GtKq.mjs} +5 -5
- package/dist/{serveManager-DX5SdhTq.mjs → serveManager-BnwAe-VX.mjs} +3 -3
- package/dist/{sideband-uW543Qst.mjs → sideband-vimBRRDF.mjs} +2 -2
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir as homedir$1 } from 'os';
|
|
2
2
|
import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
|
|
3
|
-
import { readFileSync as readFileSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, renameSync as renameSync$1, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1,
|
|
4
|
-
import path__default, { join as join$1, dirname as dirname$1, basename as basename$1
|
|
3
|
+
import { readFileSync as readFileSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, renameSync as renameSync$1, existsSync as existsSync$1, realpathSync, rmSync as rmSync$1, copyFileSync, unlinkSync as unlinkSync$1, readdirSync as readdirSync$1, watch, rmdirSync } from 'fs';
|
|
4
|
+
import path__default, { join as join$1, resolve, dirname as dirname$1, basename as basename$1 } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
@@ -10,8 +10,8 @@ import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, rename
|
|
|
10
10
|
import { exec, execSync, spawn, execFile as execFile$1, execFileSync } from 'node:child_process';
|
|
11
11
|
import { promisify } from 'util';
|
|
12
12
|
import { join, basename, dirname } from 'node:path';
|
|
13
|
-
import os, { homedir, platform } from 'node:os';
|
|
14
13
|
import { EventEmitter } from 'node:events';
|
|
14
|
+
import os, { homedir, platform } from 'node:os';
|
|
15
15
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
16
16
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
17
17
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
@@ -1388,22 +1388,36 @@ function generateSkillBody(channel, ctx) {
|
|
|
1388
1388
|
const bindNote = mode === "stateless" ? `**Stateless channel** \u2014 each call spawns a fresh, isolated one-shot session (cold-start: a few seconds), runs, replies, and closes. No shared memory between calls.` : mode === "fixed" ? `Routes to a **fixed session** (\`${rSession}\`); reachable only while that session is live.` : rSession ? `Routes to **session \`${rSession}\`** (the one this instruction was copied from); reachable only while it is live, else a fresh stateless run answers.` : `**Dynamic channel** \u2014 pass \`session: "<id>"\` to target a specific live session, else a fresh stateless run answers.`;
|
|
1389
1389
|
const name = channel.skill?.name || channel.name;
|
|
1390
1390
|
const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
|
|
1391
|
-
const replyNote = isAgent ? `This is a **WISE Agent** channel: \`send()\` runs a fast assistant against the session's tools/skills and returns its answer synchronously in the result \`reply\`.` : isQueue ? `This is an **async** channel: \`send()\` returns
|
|
1391
|
+
const replyNote = isAgent ? `This is a **WISE Agent** channel: \`send()\` runs a fast assistant against the session's tools/skills and returns its answer synchronously in the result \`reply\`.` : isQueue ? `This is an **async** channel: \`send()\` usually returns \`{ status: "queued", correlationId }\` and the agent replies later \u2014 poll \`receive()\` (or GET /receive \xB7 /events) for replies addressed to you. **But it may also answer synchronously** with \`{ status: "completed", reply }\` (e.g. when the target session is gone and a fresh one-shot run handles the call) \u2014 always check \`status\` first and use \`reply\` directly when present; \`receive()\` has nothing to return for a one-shot.` : `Delivery is fire-and-forget \u2014 the message lands in the agent's inbox, tagged with your verified identity (\`from\`).`;
|
|
1392
1392
|
const queueSection = isQueue ? `
|
|
1393
1393
|
|
|
1394
|
-
## Getting the reply
|
|
1395
|
-
\`send()\` returns
|
|
1396
|
-
|
|
1394
|
+
## Getting the reply
|
|
1395
|
+
\`send()\` returns one of two shapes \u2014 **check \`status\` first**:
|
|
1396
|
+
- \`{ status: "completed", reply, correlationId }\` \u2014 answered synchronously (e.g. a
|
|
1397
|
+
dynamic channel whose target session is gone falls back to a fresh one-shot run).
|
|
1398
|
+
Use \`reply\` directly; do **not** poll \`receive()\` \u2014 there is no persistent session
|
|
1399
|
+
to poll and it will return an error.
|
|
1400
|
+
- \`{ status: "queued", correlationId }\` \u2014 a live session will answer later; long-poll
|
|
1401
|
+
\`receive\` for replies addressed to you, advancing \`cursor\` each call:
|
|
1397
1402
|
\`\`\`js
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1403
|
+
const res = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
|
|
1404
|
+
if (res.status === "completed") return res.reply; // answered now \u2014 done
|
|
1405
|
+
let cursor = 0; // else poll for the async reply
|
|
1400
1406
|
while (true) {
|
|
1401
1407
|
const r = await get_service("${svc}").receive({ channel: "${channel.id}", key: "${key}", cursor, wait: 25 });
|
|
1402
1408
|
cursor = r.cursor;
|
|
1403
|
-
for (const reply of r.replies) if (reply.correlationId === correlationId) return reply.body;
|
|
1409
|
+
for (const reply of r.replies) if (reply.correlationId === res.correlationId) return reply.body;
|
|
1404
1410
|
}
|
|
1405
1411
|
\`\`\`
|
|
1406
|
-
**HTTP:** \`POST ${recvUrl}\` with \`{"kwargs": {"channel": "${channel.id}", "key": "${key}", "cursor": 0, "wait": 25}}\` (long-poll), or stream \`GET <channel-http>/channel/${channel.id}/events?key=${key}\` (SSE)
|
|
1412
|
+
**HTTP:** \`POST ${recvUrl}\` with \`{"kwargs": {"channel": "${channel.id}", "key": "${key}", "cursor": 0, "wait": 25}}\` (long-poll), or stream \`GET <channel-http>/channel/${channel.id}/events?key=${key}\` (SSE).
|
|
1413
|
+
|
|
1414
|
+
### Agent-to-agent: get the reply in your own inbox
|
|
1415
|
+
If **you are a Svamp agent session**, pass \`reply_to: { session: "<your-session-id>" }\`
|
|
1416
|
+
to \`send()\`. The reply is then delivered straight to **your session's inbox**
|
|
1417
|
+
(\`inbox reply\`-able, persistent) \u2014 symmetric agent\u2194agent messaging. Without
|
|
1418
|
+
\`reply_to.session\` you are treated as an **external caller**: the reply goes to this
|
|
1419
|
+
channel's outbox and you must poll \`receive()\` (above) for it \u2014 it will **not** appear
|
|
1420
|
+
in your inbox.` : "";
|
|
1407
1421
|
const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
|
|
1408
1422
|
return `---
|
|
1409
1423
|
name: ${name}
|
|
@@ -1449,7 +1463,10 @@ function resolveSender(channel, input = {}) {
|
|
|
1449
1463
|
}
|
|
1450
1464
|
if (id.mode === "per-key") {
|
|
1451
1465
|
const caller = (id.callers || []).find((c) => c.key && c.key === key);
|
|
1452
|
-
if (!caller)
|
|
1466
|
+
if (!caller) {
|
|
1467
|
+
const acceptsHypha = Array.isArray(id.hypha_allow) && id.hypha_allow.length > 0;
|
|
1468
|
+
return { error: acceptsHypha ? "invalid or missing key \u2014 supply a caller key issued by the channel owner, or call with an authenticated Hypha identity (this channel accepts hypha_allow callers; anonymous/keyless requests are rejected)" : "invalid or missing key \u2014 this channel requires a caller key issued by the channel owner" };
|
|
1469
|
+
}
|
|
1453
1470
|
return { sender: { name: caller.name, kind: caller.kind, verified: true } };
|
|
1454
1471
|
}
|
|
1455
1472
|
if (id.mode === "caller-supplied") {
|
|
@@ -1482,6 +1499,138 @@ function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, no
|
|
|
1482
1499
|
return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
|
|
1483
1500
|
}
|
|
1484
1501
|
|
|
1502
|
+
const MAX_PER_CHANNEL = 200;
|
|
1503
|
+
const TTL_MS = 60 * 60 * 1e3;
|
|
1504
|
+
class ChannelOutbox {
|
|
1505
|
+
file;
|
|
1506
|
+
byChannel = /* @__PURE__ */ new Map();
|
|
1507
|
+
seqByChannel = /* @__PURE__ */ new Map();
|
|
1508
|
+
emitter = new EventEmitter();
|
|
1509
|
+
constructor(projectDir) {
|
|
1510
|
+
const dir = join(projectDir, ".svamp", "channels");
|
|
1511
|
+
this.file = join(dir, "_outbox.jsonl");
|
|
1512
|
+
this.emitter.setMaxListeners(0);
|
|
1513
|
+
try {
|
|
1514
|
+
mkdirSync(dir, { recursive: true });
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
this._load();
|
|
1518
|
+
}
|
|
1519
|
+
_load() {
|
|
1520
|
+
if (!existsSync(this.file)) return;
|
|
1521
|
+
const cutoff = Date.now() - TTL_MS;
|
|
1522
|
+
try {
|
|
1523
|
+
for (const line of readFileSync(this.file, "utf8").split("\n")) {
|
|
1524
|
+
if (!line.trim()) continue;
|
|
1525
|
+
let rec = null;
|
|
1526
|
+
try {
|
|
1527
|
+
rec = JSON.parse(line);
|
|
1528
|
+
} catch {
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
if (!rec || rec.ts < cutoff) continue;
|
|
1532
|
+
const { channelId, ...reply } = rec;
|
|
1533
|
+
const arr = this.byChannel.get(channelId) || [];
|
|
1534
|
+
arr.push(reply);
|
|
1535
|
+
this.byChannel.set(channelId, arr);
|
|
1536
|
+
this.seqByChannel.set(channelId, Math.max(this.seqByChannel.get(channelId) || 0, reply.seq));
|
|
1537
|
+
}
|
|
1538
|
+
for (const [ch, arr] of this.byChannel) if (arr.length > MAX_PER_CHANNEL) this.byChannel.set(ch, arr.slice(-MAX_PER_CHANNEL));
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
_evict(channelId) {
|
|
1543
|
+
const cutoff = Date.now() - TTL_MS;
|
|
1544
|
+
let arr = this.byChannel.get(channelId);
|
|
1545
|
+
if (!arr) return;
|
|
1546
|
+
if (arr.some((r) => r.ts < cutoff)) arr = arr.filter((r) => r.ts >= cutoff);
|
|
1547
|
+
if (arr.length > MAX_PER_CHANNEL) arr = arr.slice(-MAX_PER_CHANNEL);
|
|
1548
|
+
this.byChannel.set(channelId, arr);
|
|
1549
|
+
}
|
|
1550
|
+
/** Append a reply addressed to `to`. Assigns seq + ts, persists, and wakes waiters. */
|
|
1551
|
+
append(channelId, r) {
|
|
1552
|
+
const seq = (this.seqByChannel.get(channelId) || 0) + 1;
|
|
1553
|
+
this.seqByChannel.set(channelId, seq);
|
|
1554
|
+
const reply = { seq, ts: Date.now(), to: r.to, body: r.body, ...r.correlationId ? { correlationId: r.correlationId } : {} };
|
|
1555
|
+
const arr = this.byChannel.get(channelId) || [];
|
|
1556
|
+
arr.push(reply);
|
|
1557
|
+
this.byChannel.set(channelId, arr);
|
|
1558
|
+
this._evict(channelId);
|
|
1559
|
+
try {
|
|
1560
|
+
appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
|
|
1561
|
+
} catch {
|
|
1562
|
+
try {
|
|
1563
|
+
mkdirSync(join(this.file, ".."), { recursive: true });
|
|
1564
|
+
appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
this.emitter.emit(channelId, reply);
|
|
1569
|
+
return reply;
|
|
1570
|
+
}
|
|
1571
|
+
/** Replies for `to` on `channelId` with seq > cursor (optionally one correlationId). */
|
|
1572
|
+
since(channelId, cursor, to, correlationId) {
|
|
1573
|
+
this._evict(channelId);
|
|
1574
|
+
return (this.byChannel.get(channelId) || []).filter((r) => r.seq > cursor && r.to === to && (!correlationId || r.correlationId === correlationId));
|
|
1575
|
+
}
|
|
1576
|
+
/** Highest seq on a channel (the cursor a caller gets back). */
|
|
1577
|
+
cursor(channelId) {
|
|
1578
|
+
return this.seqByChannel.get(channelId) || 0;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Long-poll: resolve immediately if there are replies after `cursor`, else wait for
|
|
1582
|
+
* the next append (filtered to this channel + `to`) or `timeoutMs`, then return.
|
|
1583
|
+
*/
|
|
1584
|
+
wait(channelId, cursor, to, timeoutMs, correlationId) {
|
|
1585
|
+
const ready = this.since(channelId, cursor, to, correlationId);
|
|
1586
|
+
if (ready.length) return Promise.resolve({ replies: ready, cursor: this.cursor(channelId) });
|
|
1587
|
+
return new Promise((resolve) => {
|
|
1588
|
+
const onReply = (r) => {
|
|
1589
|
+
if (r.to !== to || correlationId && r.correlationId !== correlationId) return;
|
|
1590
|
+
cleanup();
|
|
1591
|
+
resolve({ replies: this.since(channelId, cursor, to, correlationId), cursor: this.cursor(channelId) });
|
|
1592
|
+
};
|
|
1593
|
+
const timer = setTimeout(() => {
|
|
1594
|
+
cleanup();
|
|
1595
|
+
resolve({ replies: [], cursor: this.cursor(channelId) });
|
|
1596
|
+
}, Math.max(0, timeoutMs));
|
|
1597
|
+
const cleanup = () => {
|
|
1598
|
+
clearTimeout(timer);
|
|
1599
|
+
this.emitter.off(channelId, onReply);
|
|
1600
|
+
};
|
|
1601
|
+
this.emitter.on(channelId, onReply);
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
/** Push subscription for SSE: calls onReply for each new reply addressed to `to`. */
|
|
1605
|
+
subscribe(channelId, to, onReply) {
|
|
1606
|
+
const handler = (r) => {
|
|
1607
|
+
if (r.to === to) onReply(r);
|
|
1608
|
+
};
|
|
1609
|
+
this.emitter.on(channelId, handler);
|
|
1610
|
+
return () => this.emitter.off(channelId, handler);
|
|
1611
|
+
}
|
|
1612
|
+
/** Drop a channel's outbox (on channel delete) — best-effort rewrite of the log. */
|
|
1613
|
+
purge(channelId) {
|
|
1614
|
+
this.byChannel.delete(channelId);
|
|
1615
|
+
this.seqByChannel.delete(channelId);
|
|
1616
|
+
if (!existsSync(this.file)) return;
|
|
1617
|
+
try {
|
|
1618
|
+
const kept = readFileSync(this.file, "utf8").split("\n").filter((l) => {
|
|
1619
|
+
if (!l.trim()) return false;
|
|
1620
|
+
try {
|
|
1621
|
+
return JSON.parse(l).channelId !== channelId;
|
|
1622
|
+
} catch {
|
|
1623
|
+
return false;
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
const tmp = this.file + ".tmp";
|
|
1627
|
+
writeFileSync(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
|
|
1628
|
+
renameSync(tmp, this.file);
|
|
1629
|
+
} catch {
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1485
1634
|
function getParamNames(fn) {
|
|
1486
1635
|
const src = fn.toString();
|
|
1487
1636
|
const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
|
|
@@ -2527,7 +2676,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
2527
2676
|
const tunnels = handlers.tunnels;
|
|
2528
2677
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
2529
2678
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
2530
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
2679
|
+
const { FrpcTunnel } = await import('./frpc-1aSnPVrE.mjs');
|
|
2531
2680
|
const tunnel = new FrpcTunnel({
|
|
2532
2681
|
name: params.name,
|
|
2533
2682
|
ports: params.ports,
|
|
@@ -2788,7 +2937,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2788
2937
|
}
|
|
2789
2938
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
2790
2939
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
2791
|
-
const { toolsForRole } = await import('./sideband-
|
|
2940
|
+
const { toolsForRole } = await import('./sideband-vimBRRDF.mjs');
|
|
2792
2941
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
2793
2942
|
return fmt(r2);
|
|
2794
2943
|
}
|
|
@@ -2887,7 +3036,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2887
3036
|
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
2888
3037
|
const callId = "call_" + Math.random().toString(16).slice(2, 12);
|
|
2889
3038
|
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
2890
|
-
const { queryCore } = await import('./commands-
|
|
3039
|
+
const { queryCore } = await import('./commands-B3NhziMR.mjs');
|
|
2891
3040
|
const timeout = c.reply?.timeout_sec || 120;
|
|
2892
3041
|
let result;
|
|
2893
3042
|
try {
|
|
@@ -2904,7 +3053,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2904
3053
|
}
|
|
2905
3054
|
if (result.status === "error") return { ok: false, call_id: callId, status: "error", error: result.error };
|
|
2906
3055
|
if (result.status === "permission-pending") return { ok: false, call_id: callId, status: "permission-pending", error: result.error };
|
|
2907
|
-
return { ok: true, call_id: callId, status: "completed", reply: result.response };
|
|
3056
|
+
return { ok: true, call_id: callId, correlationId: callId, status: "completed", reply: result.response };
|
|
2908
3057
|
};
|
|
2909
3058
|
const channelsServiceInfo = await server.registerService(
|
|
2910
3059
|
{
|
|
@@ -2925,10 +3074,16 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2925
3074
|
}
|
|
2926
3075
|
return out;
|
|
2927
3076
|
},
|
|
2928
|
-
|
|
2929
|
-
|
|
3077
|
+
// Accept a plain `?channel=<id>` GET (positional, like `skill`) AS WELL AS
|
|
3078
|
+
// the kwargs-wrapped form. The Hypha HTTP gateway only maps a `?channel=`
|
|
3079
|
+
// query onto a param literally named `channel`; a kwargs-only signature left
|
|
3080
|
+
// `channel` undefined on GET, so describe() 404'd on every channel that
|
|
3081
|
+
// list() returned. Normalize both the string and `{channel}` object shapes.
|
|
3082
|
+
describe: async (channel) => {
|
|
3083
|
+
const id = typeof channel === "string" ? channel : channel?.channel;
|
|
3084
|
+
const rpc = id ? await findChannelOwner(id) : void 0;
|
|
2930
3085
|
if (!rpc?.channelDescribe) return { error: "channel not found" };
|
|
2931
|
-
return rpc.channelDescribe(
|
|
3086
|
+
return rpc.channelDescribe(id);
|
|
2932
3087
|
},
|
|
2933
3088
|
// Clean GET endpoint for the self-contained skill markdown. Param is
|
|
2934
3089
|
// named `channel` (not `kwargs`) so the Hypha HTTP gateway maps a plain
|
|
@@ -2958,9 +3113,26 @@ ${d?.error || "not found"}`;
|
|
|
2958
3113
|
trackInbound();
|
|
2959
3114
|
const res = resolveChannel(kwargs.channel, kwargs.session);
|
|
2960
3115
|
if ("error" in res) return { error: res.error };
|
|
2961
|
-
if (res.tier === "
|
|
2962
|
-
|
|
2963
|
-
|
|
3116
|
+
if (res.tier === "session" && !("stopped" in res)) {
|
|
3117
|
+
return res.rpc.channelReceive({ channel: kwargs.channel, key: kwargs.key, from: kwargs.from, cursor: kwargs.cursor, correlationId: kwargs.correlationId, wait: kwargs.wait }, context);
|
|
3118
|
+
}
|
|
3119
|
+
const { c, dir } = res;
|
|
3120
|
+
if (c.reply?.mode !== "queue") {
|
|
3121
|
+
return { error: "this channel has no async replies (not a queue-mode channel)" };
|
|
3122
|
+
}
|
|
3123
|
+
const u = context?.user;
|
|
3124
|
+
const r = resolveSender(c, {
|
|
3125
|
+
key: kwargs.key,
|
|
3126
|
+
from: kwargs.from,
|
|
3127
|
+
hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
|
|
3128
|
+
hyphaAnonymous: u?.is_anonymous === true,
|
|
3129
|
+
hyphaWorkspace: u?.scope?.current_workspace
|
|
3130
|
+
});
|
|
3131
|
+
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
3132
|
+
const cursor = Math.max(0, Number(kwargs.cursor) || 0);
|
|
3133
|
+
const outbox = new ChannelOutbox(dir);
|
|
3134
|
+
const replies = outbox.since(c.id, cursor, r.sender.name, kwargs.correlationId);
|
|
3135
|
+
return { ok: true, replies, cursor: outbox.cursor(c.id) };
|
|
2964
3136
|
}
|
|
2965
3137
|
},
|
|
2966
3138
|
{ overwrite: true }
|
|
@@ -3202,138 +3374,6 @@ class RoutineRunner {
|
|
|
3202
3374
|
}
|
|
3203
3375
|
}
|
|
3204
3376
|
|
|
3205
|
-
const MAX_PER_CHANNEL = 200;
|
|
3206
|
-
const TTL_MS = 60 * 60 * 1e3;
|
|
3207
|
-
class ChannelOutbox {
|
|
3208
|
-
file;
|
|
3209
|
-
byChannel = /* @__PURE__ */ new Map();
|
|
3210
|
-
seqByChannel = /* @__PURE__ */ new Map();
|
|
3211
|
-
emitter = new EventEmitter();
|
|
3212
|
-
constructor(projectDir) {
|
|
3213
|
-
const dir = join(projectDir, ".svamp", "channels");
|
|
3214
|
-
this.file = join(dir, "_outbox.jsonl");
|
|
3215
|
-
this.emitter.setMaxListeners(0);
|
|
3216
|
-
try {
|
|
3217
|
-
mkdirSync(dir, { recursive: true });
|
|
3218
|
-
} catch {
|
|
3219
|
-
}
|
|
3220
|
-
this._load();
|
|
3221
|
-
}
|
|
3222
|
-
_load() {
|
|
3223
|
-
if (!existsSync(this.file)) return;
|
|
3224
|
-
const cutoff = Date.now() - TTL_MS;
|
|
3225
|
-
try {
|
|
3226
|
-
for (const line of readFileSync(this.file, "utf8").split("\n")) {
|
|
3227
|
-
if (!line.trim()) continue;
|
|
3228
|
-
let rec = null;
|
|
3229
|
-
try {
|
|
3230
|
-
rec = JSON.parse(line);
|
|
3231
|
-
} catch {
|
|
3232
|
-
continue;
|
|
3233
|
-
}
|
|
3234
|
-
if (!rec || rec.ts < cutoff) continue;
|
|
3235
|
-
const { channelId, ...reply } = rec;
|
|
3236
|
-
const arr = this.byChannel.get(channelId) || [];
|
|
3237
|
-
arr.push(reply);
|
|
3238
|
-
this.byChannel.set(channelId, arr);
|
|
3239
|
-
this.seqByChannel.set(channelId, Math.max(this.seqByChannel.get(channelId) || 0, reply.seq));
|
|
3240
|
-
}
|
|
3241
|
-
for (const [ch, arr] of this.byChannel) if (arr.length > MAX_PER_CHANNEL) this.byChannel.set(ch, arr.slice(-MAX_PER_CHANNEL));
|
|
3242
|
-
} catch {
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
_evict(channelId) {
|
|
3246
|
-
const cutoff = Date.now() - TTL_MS;
|
|
3247
|
-
let arr = this.byChannel.get(channelId);
|
|
3248
|
-
if (!arr) return;
|
|
3249
|
-
if (arr.some((r) => r.ts < cutoff)) arr = arr.filter((r) => r.ts >= cutoff);
|
|
3250
|
-
if (arr.length > MAX_PER_CHANNEL) arr = arr.slice(-MAX_PER_CHANNEL);
|
|
3251
|
-
this.byChannel.set(channelId, arr);
|
|
3252
|
-
}
|
|
3253
|
-
/** Append a reply addressed to `to`. Assigns seq + ts, persists, and wakes waiters. */
|
|
3254
|
-
append(channelId, r) {
|
|
3255
|
-
const seq = (this.seqByChannel.get(channelId) || 0) + 1;
|
|
3256
|
-
this.seqByChannel.set(channelId, seq);
|
|
3257
|
-
const reply = { seq, ts: Date.now(), to: r.to, body: r.body, ...r.correlationId ? { correlationId: r.correlationId } : {} };
|
|
3258
|
-
const arr = this.byChannel.get(channelId) || [];
|
|
3259
|
-
arr.push(reply);
|
|
3260
|
-
this.byChannel.set(channelId, arr);
|
|
3261
|
-
this._evict(channelId);
|
|
3262
|
-
try {
|
|
3263
|
-
appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
|
|
3264
|
-
} catch {
|
|
3265
|
-
try {
|
|
3266
|
-
mkdirSync(join(this.file, ".."), { recursive: true });
|
|
3267
|
-
appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
|
|
3268
|
-
} catch {
|
|
3269
|
-
}
|
|
3270
|
-
}
|
|
3271
|
-
this.emitter.emit(channelId, reply);
|
|
3272
|
-
return reply;
|
|
3273
|
-
}
|
|
3274
|
-
/** Replies for `to` on `channelId` with seq > cursor (optionally one correlationId). */
|
|
3275
|
-
since(channelId, cursor, to, correlationId) {
|
|
3276
|
-
this._evict(channelId);
|
|
3277
|
-
return (this.byChannel.get(channelId) || []).filter((r) => r.seq > cursor && r.to === to && (!correlationId || r.correlationId === correlationId));
|
|
3278
|
-
}
|
|
3279
|
-
/** Highest seq on a channel (the cursor a caller gets back). */
|
|
3280
|
-
cursor(channelId) {
|
|
3281
|
-
return this.seqByChannel.get(channelId) || 0;
|
|
3282
|
-
}
|
|
3283
|
-
/**
|
|
3284
|
-
* Long-poll: resolve immediately if there are replies after `cursor`, else wait for
|
|
3285
|
-
* the next append (filtered to this channel + `to`) or `timeoutMs`, then return.
|
|
3286
|
-
*/
|
|
3287
|
-
wait(channelId, cursor, to, timeoutMs, correlationId) {
|
|
3288
|
-
const ready = this.since(channelId, cursor, to, correlationId);
|
|
3289
|
-
if (ready.length) return Promise.resolve({ replies: ready, cursor: this.cursor(channelId) });
|
|
3290
|
-
return new Promise((resolve) => {
|
|
3291
|
-
const onReply = (r) => {
|
|
3292
|
-
if (r.to !== to || correlationId && r.correlationId !== correlationId) return;
|
|
3293
|
-
cleanup();
|
|
3294
|
-
resolve({ replies: this.since(channelId, cursor, to, correlationId), cursor: this.cursor(channelId) });
|
|
3295
|
-
};
|
|
3296
|
-
const timer = setTimeout(() => {
|
|
3297
|
-
cleanup();
|
|
3298
|
-
resolve({ replies: [], cursor: this.cursor(channelId) });
|
|
3299
|
-
}, Math.max(0, timeoutMs));
|
|
3300
|
-
const cleanup = () => {
|
|
3301
|
-
clearTimeout(timer);
|
|
3302
|
-
this.emitter.off(channelId, onReply);
|
|
3303
|
-
};
|
|
3304
|
-
this.emitter.on(channelId, onReply);
|
|
3305
|
-
});
|
|
3306
|
-
}
|
|
3307
|
-
/** Push subscription for SSE: calls onReply for each new reply addressed to `to`. */
|
|
3308
|
-
subscribe(channelId, to, onReply) {
|
|
3309
|
-
const handler = (r) => {
|
|
3310
|
-
if (r.to === to) onReply(r);
|
|
3311
|
-
};
|
|
3312
|
-
this.emitter.on(channelId, handler);
|
|
3313
|
-
return () => this.emitter.off(channelId, handler);
|
|
3314
|
-
}
|
|
3315
|
-
/** Drop a channel's outbox (on channel delete) — best-effort rewrite of the log. */
|
|
3316
|
-
purge(channelId) {
|
|
3317
|
-
this.byChannel.delete(channelId);
|
|
3318
|
-
this.seqByChannel.delete(channelId);
|
|
3319
|
-
if (!existsSync(this.file)) return;
|
|
3320
|
-
try {
|
|
3321
|
-
const kept = readFileSync(this.file, "utf8").split("\n").filter((l) => {
|
|
3322
|
-
if (!l.trim()) return false;
|
|
3323
|
-
try {
|
|
3324
|
-
return JSON.parse(l).channelId !== channelId;
|
|
3325
|
-
} catch {
|
|
3326
|
-
return false;
|
|
3327
|
-
}
|
|
3328
|
-
});
|
|
3329
|
-
const tmp = this.file + ".tmp";
|
|
3330
|
-
writeFileSync(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
|
|
3331
|
-
renameSync(tmp, this.file);
|
|
3332
|
-
} catch {
|
|
3333
|
-
}
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
|
|
3337
3377
|
function channelPublicView(c) {
|
|
3338
3378
|
return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind, bind: c.bind };
|
|
3339
3379
|
}
|
|
@@ -3439,6 +3479,48 @@ function appendMessage(messagesDir, sessionId, msg) {
|
|
|
3439
3479
|
console.error(`[HYPHA SESSION ${sessionId}] Failed to persist message: ${err?.message ?? err}`);
|
|
3440
3480
|
}
|
|
3441
3481
|
}
|
|
3482
|
+
function editableInfo(msg) {
|
|
3483
|
+
const c = msg?.content;
|
|
3484
|
+
if (!c) return null;
|
|
3485
|
+
if (c.role === "user" && c.content?.type === "text") {
|
|
3486
|
+
return { role: "user", text: String(c.content.text ?? "") };
|
|
3487
|
+
}
|
|
3488
|
+
if (c.role === "agent" && c.content?.type === "output") {
|
|
3489
|
+
const data = c.content.data;
|
|
3490
|
+
if (data?.type === "assistant" && Array.isArray(data?.message?.content)) {
|
|
3491
|
+
const textBlocks = data.message.content.filter((b) => b && b.type === "text" && typeof b.text === "string");
|
|
3492
|
+
if (textBlocks.length >= 1) {
|
|
3493
|
+
return { role: "assistant", text: textBlocks.map((b) => b.text).join("\n") };
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
if (data?.type === "user" && typeof data?.message?.content === "string") {
|
|
3497
|
+
return { role: "user", text: data.message.content };
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
return null;
|
|
3501
|
+
}
|
|
3502
|
+
function patchStoredText(msg, newText) {
|
|
3503
|
+
const c = msg?.content;
|
|
3504
|
+
if (c?.role === "user" && c.content?.type === "text") {
|
|
3505
|
+
c.content.text = newText;
|
|
3506
|
+
return true;
|
|
3507
|
+
}
|
|
3508
|
+
if (c?.role === "agent" && c.content?.type === "output") {
|
|
3509
|
+
const data = c.content.data;
|
|
3510
|
+
if (data?.type === "user" && typeof data?.message?.content === "string") {
|
|
3511
|
+
data.message.content = newText;
|
|
3512
|
+
return true;
|
|
3513
|
+
}
|
|
3514
|
+
if (data?.type === "assistant" && Array.isArray(data?.message?.content)) {
|
|
3515
|
+
const textBlocks = data.message.content.filter((b) => b && b.type === "text" && typeof b.text === "string");
|
|
3516
|
+
if (textBlocks.length === 1) {
|
|
3517
|
+
textBlocks[0].text = newText;
|
|
3518
|
+
return true;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
return false;
|
|
3523
|
+
}
|
|
3442
3524
|
function createSessionStore(server, sessionId, initialMetadata, initialAgentState, callbacks, options) {
|
|
3443
3525
|
const messages = options?.messagesDir ? loadMessages(options.messagesDir) : [];
|
|
3444
3526
|
let nextSeq = messages.length > 0 ? messages[messages.length - 1].seq + 1 : 1;
|
|
@@ -3584,6 +3666,100 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
3584
3666
|
notifyListeners({ type: "update-session", sessionId, metadata: { value: metadata, version: metadataVersion } });
|
|
3585
3667
|
callbacks.onMetadataUpdate?.(metadata);
|
|
3586
3668
|
};
|
|
3669
|
+
const forkClaudePrint = async (prompt, claudeSessionId, cwd, maxTurns = 6) => {
|
|
3670
|
+
const { spawn } = await import('child_process');
|
|
3671
|
+
return new Promise((resolve) => {
|
|
3672
|
+
const child = spawn("claude", [
|
|
3673
|
+
"--print",
|
|
3674
|
+
prompt,
|
|
3675
|
+
"--resume",
|
|
3676
|
+
claudeSessionId,
|
|
3677
|
+
"--fork-session",
|
|
3678
|
+
"--no-session-persistence",
|
|
3679
|
+
"--permission-mode",
|
|
3680
|
+
"bypassPermissions",
|
|
3681
|
+
"--output-format",
|
|
3682
|
+
"json",
|
|
3683
|
+
"--max-turns",
|
|
3684
|
+
String(maxTurns)
|
|
3685
|
+
], {
|
|
3686
|
+
cwd,
|
|
3687
|
+
timeout: 6e4,
|
|
3688
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3689
|
+
env: { ...process.env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" }
|
|
3690
|
+
});
|
|
3691
|
+
let stdout = "";
|
|
3692
|
+
let stderr = "";
|
|
3693
|
+
child.stdout?.on("data", (d) => {
|
|
3694
|
+
stdout += d.toString();
|
|
3695
|
+
});
|
|
3696
|
+
child.stderr?.on("data", (d) => {
|
|
3697
|
+
stderr += d.toString();
|
|
3698
|
+
});
|
|
3699
|
+
child.on("close", (code) => {
|
|
3700
|
+
if (code !== 0 && !stdout) {
|
|
3701
|
+
resolve({ success: false, error: stderr || `claude exited with code ${code}` });
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
try {
|
|
3705
|
+
const result = JSON.parse(stdout);
|
|
3706
|
+
resolve({ success: true, text: result.result || result.text || stdout });
|
|
3707
|
+
} catch {
|
|
3708
|
+
resolve({ success: true, text: stdout.trim() });
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
child.on("error", (err) => resolve({ success: false, error: err.message }));
|
|
3712
|
+
});
|
|
3713
|
+
};
|
|
3714
|
+
const performEdit = async (target, newText) => {
|
|
3715
|
+
if (!callbacks.onEditTranscript) return { success: false, message: "Editing history is not supported for this session." };
|
|
3716
|
+
const info = editableInfo(target);
|
|
3717
|
+
if (!info) return { success: false, message: "This message cannot be edited." };
|
|
3718
|
+
const text = typeof newText === "string" ? newText : "";
|
|
3719
|
+
if (!text.trim()) return { success: false, message: "New text is empty." };
|
|
3720
|
+
let fromEnd = 0;
|
|
3721
|
+
for (const m of messages) {
|
|
3722
|
+
if (m.seq <= target.seq) continue;
|
|
3723
|
+
const i2 = editableInfo(m);
|
|
3724
|
+
if (i2 && i2.role === info.role) fromEnd++;
|
|
3725
|
+
}
|
|
3726
|
+
const anchorSeq = target.seq;
|
|
3727
|
+
const applyStoreA = () => {
|
|
3728
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3729
|
+
if (messages[i].seq > anchorSeq) messages.splice(i, 1);
|
|
3730
|
+
}
|
|
3731
|
+
const tgt = messages.find((m) => m.seq === anchorSeq);
|
|
3732
|
+
if (tgt) {
|
|
3733
|
+
patchStoredText(tgt, text);
|
|
3734
|
+
tgt.updatedAt = Date.now();
|
|
3735
|
+
}
|
|
3736
|
+
nextSeq = anchorSeq + 1;
|
|
3737
|
+
if (options?.messagesDir) {
|
|
3738
|
+
const filePath = join(options.messagesDir, "messages.jsonl");
|
|
3739
|
+
try {
|
|
3740
|
+
const lines = existsSync(filePath) ? readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim()) : [];
|
|
3741
|
+
const kept = [];
|
|
3742
|
+
for (const line of lines) {
|
|
3743
|
+
let m;
|
|
3744
|
+
try {
|
|
3745
|
+
m = JSON.parse(line);
|
|
3746
|
+
} catch {
|
|
3747
|
+
continue;
|
|
3748
|
+
}
|
|
3749
|
+
if (typeof m.seq === "number" && m.seq > anchorSeq) continue;
|
|
3750
|
+
kept.push(m.seq === anchorSeq && tgt ? JSON.stringify(tgt) : line);
|
|
3751
|
+
}
|
|
3752
|
+
const tmp = `${filePath}.tmp-${process.pid}`;
|
|
3753
|
+
writeFileSync(tmp, kept.length ? kept.join("\n") + "\n" : "");
|
|
3754
|
+
renameSync(tmp, filePath);
|
|
3755
|
+
} catch (err) {
|
|
3756
|
+
console.error(`[HYPHA SESSION ${sessionId}] Store A rewrite failed: ${err?.message ?? err}`);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
notifyListeners({ type: "messages-edited", sessionId, latestSeq: nextSeq - 1 });
|
|
3760
|
+
};
|
|
3761
|
+
return callbacks.onEditTranscript({ role: info.role, fromEnd, oldText: info.text, newText: text, applyStoreA });
|
|
3762
|
+
};
|
|
3587
3763
|
const rpcHandlers = {
|
|
3588
3764
|
// ── Messages ──
|
|
3589
3765
|
getMessages: async (afterSeq, limit, context) => {
|
|
@@ -3870,7 +4046,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
3870
4046
|
const result = await dispatchAgentChannel({ channel: c, sender: r.sender, message: rendered, deps, env: process.env });
|
|
3871
4047
|
channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: result.status });
|
|
3872
4048
|
syncChannelsToMetadata();
|
|
3873
|
-
return { ok: result.status === "completed", call_id: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
|
|
4049
|
+
return { ok: result.status === "completed", call_id: callId, correlationId: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
|
|
3874
4050
|
}
|
|
3875
4051
|
if (c.action?.kind === "loop") return { error: "loop channels are served by the channel server, not channelSend" };
|
|
3876
4052
|
const queue = c.reply?.mode === "queue";
|
|
@@ -4263,59 +4439,72 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
4263
4439
|
if (!claudeSessionId) {
|
|
4264
4440
|
return { success: false, error: "No active Claude session to query" };
|
|
4265
4441
|
}
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
const
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4442
|
+
const r = await forkClaudePrint(question, claudeSessionId, metadata.path || process.cwd());
|
|
4443
|
+
return r.success ? { success: true, answer: r.text } : { success: false, error: r.error };
|
|
4444
|
+
},
|
|
4445
|
+
editMessage: async (messageId, newText, context) => {
|
|
4446
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
4447
|
+
if (!messageId) return { success: false, message: "messageId is required." };
|
|
4448
|
+
const target = messages.find((m) => m.id === messageId);
|
|
4449
|
+
if (!target) {
|
|
4450
|
+
return { success: false, message: "Message not found in the active window \u2014 only recent messages can be edited." };
|
|
4451
|
+
}
|
|
4452
|
+
return performEdit(target, newText);
|
|
4453
|
+
},
|
|
4454
|
+
refineLastReply: async (instruction, context) => {
|
|
4455
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
4456
|
+
if (!instruction || typeof instruction !== "string") {
|
|
4457
|
+
return { success: false, message: "An instruction is required." };
|
|
4458
|
+
}
|
|
4459
|
+
const claudeSessionId = metadata.claudeSessionId;
|
|
4460
|
+
if (!claudeSessionId) return { success: false, message: "No active Claude session to refine." };
|
|
4461
|
+
let last;
|
|
4462
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
4463
|
+
const info = editableInfo(messages[i]);
|
|
4464
|
+
if (info && info.role === "assistant") {
|
|
4465
|
+
last = messages[i];
|
|
4466
|
+
break;
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
if (!last) return { success: false, message: "No assistant reply to refine." };
|
|
4470
|
+
const oldText = editableInfo(last).text;
|
|
4471
|
+
const prompt = `You previously wrote this reply:
|
|
4472
|
+
|
|
4473
|
+
<previous_reply>
|
|
4474
|
+
${oldText}
|
|
4475
|
+
</previous_reply>
|
|
4476
|
+
|
|
4477
|
+
Revise it according to this instruction:
|
|
4478
|
+
<instruction>
|
|
4479
|
+
${instruction}
|
|
4480
|
+
</instruction>
|
|
4481
|
+
|
|
4482
|
+
Output ONLY the full revised reply text \u2014 no preamble, no commentary, no surrounding quotes or code fences.`;
|
|
4483
|
+
const revised = await forkClaudePrint(prompt, claudeSessionId, metadata.path || process.cwd());
|
|
4484
|
+
if (!revised.success || !revised.text?.trim()) {
|
|
4485
|
+
return { success: false, message: revised.error || "Failed to generate a revised reply." };
|
|
4486
|
+
}
|
|
4487
|
+
return performEdit(last, revised.text.trim());
|
|
4488
|
+
},
|
|
4489
|
+
undoLastEdit: async (context) => {
|
|
4490
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
4491
|
+
if (!callbacks.onUndoEdit) return { success: false, message: "Undo is not supported for this session." };
|
|
4492
|
+
const restoreStoreA = () => {
|
|
4493
|
+
if (!options?.messagesDir) return;
|
|
4494
|
+
try {
|
|
4495
|
+
const undoBackup = join(options.messagesDir, "undo", "messages.jsonl");
|
|
4496
|
+
const target = join(options.messagesDir, "messages.jsonl");
|
|
4497
|
+
if (existsSync(undoBackup)) writeFileSync(target, readFileSync(undoBackup));
|
|
4498
|
+
const reloaded = loadMessages(options.messagesDir, sessionId);
|
|
4499
|
+
messages.length = 0;
|
|
4500
|
+
messages.push(...reloaded);
|
|
4501
|
+
nextSeq = messages.length ? messages[messages.length - 1].seq + 1 : 1;
|
|
4502
|
+
notifyListeners({ type: "messages-edited", sessionId, latestSeq: nextSeq - 1 });
|
|
4503
|
+
} catch (err) {
|
|
4504
|
+
console.error(`[HYPHA SESSION ${sessionId}] Undo Store A restore failed: ${err?.message ?? err}`);
|
|
4505
|
+
}
|
|
4506
|
+
};
|
|
4507
|
+
return callbacks.onUndoEdit({ restoreStoreA });
|
|
4319
4508
|
}
|
|
4320
4509
|
};
|
|
4321
4510
|
const store = {
|
|
@@ -7149,6 +7338,171 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
7149
7338
|
GeminiTransport: GeminiTransport
|
|
7150
7339
|
});
|
|
7151
7340
|
|
|
7341
|
+
function resolveTranscriptPath(cwd, claudeSessionId) {
|
|
7342
|
+
let real;
|
|
7343
|
+
try {
|
|
7344
|
+
real = realpathSync(cwd);
|
|
7345
|
+
} catch {
|
|
7346
|
+
real = resolve(cwd);
|
|
7347
|
+
}
|
|
7348
|
+
const projectId = real.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
7349
|
+
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join$1(os$1.homedir(), ".claude");
|
|
7350
|
+
return join$1(claudeConfigDir, "projects", projectId, `${claudeSessionId}.jsonl`);
|
|
7351
|
+
}
|
|
7352
|
+
function normalizeText(t) {
|
|
7353
|
+
return (t || "").replace(/\s+/g, " ").trim();
|
|
7354
|
+
}
|
|
7355
|
+
function parseTranscript(file) {
|
|
7356
|
+
const content = readFileSync$1(file, "utf-8");
|
|
7357
|
+
return content.split("\n").filter((l) => l.trim().length > 0).map((raw) => {
|
|
7358
|
+
let obj = null;
|
|
7359
|
+
try {
|
|
7360
|
+
obj = JSON.parse(raw);
|
|
7361
|
+
} catch {
|
|
7362
|
+
}
|
|
7363
|
+
return { raw, obj };
|
|
7364
|
+
});
|
|
7365
|
+
}
|
|
7366
|
+
function assistantText(obj) {
|
|
7367
|
+
const content = obj?.message?.content;
|
|
7368
|
+
if (typeof content === "string") return content;
|
|
7369
|
+
if (!Array.isArray(content)) return "";
|
|
7370
|
+
return content.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
7371
|
+
}
|
|
7372
|
+
function isRealUserRecord(obj) {
|
|
7373
|
+
return !!obj && obj.type === "user" && !obj.isSidechain && typeof obj?.message?.content === "string";
|
|
7374
|
+
}
|
|
7375
|
+
function isAgentTextRecord(obj) {
|
|
7376
|
+
if (!obj || obj.type !== "assistant" || obj.isSidechain) return false;
|
|
7377
|
+
const content = obj?.message?.content;
|
|
7378
|
+
if (!Array.isArray(content)) return false;
|
|
7379
|
+
return content.some((b) => b && b.type === "text" && typeof b.text === "string");
|
|
7380
|
+
}
|
|
7381
|
+
function isRoleTextRecord(obj, role) {
|
|
7382
|
+
return role === "user" ? isRealUserRecord(obj) : isAgentTextRecord(obj);
|
|
7383
|
+
}
|
|
7384
|
+
function recordText(obj, role) {
|
|
7385
|
+
return role === "user" ? String(obj?.message?.content ?? "") : assistantText(obj);
|
|
7386
|
+
}
|
|
7387
|
+
function findAnchorIndex(lines, anchor) {
|
|
7388
|
+
const roleIdx = [];
|
|
7389
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7390
|
+
if (isRoleTextRecord(lines[i].obj, anchor.role)) roleIdx.push(i);
|
|
7391
|
+
}
|
|
7392
|
+
if (roleIdx.length === 0) {
|
|
7393
|
+
return { ok: false, reason: `no ${anchor.role} text records in transcript` };
|
|
7394
|
+
}
|
|
7395
|
+
const pos = roleIdx.length - 1 - anchor.fromEnd;
|
|
7396
|
+
if (pos < 0 || pos >= roleIdx.length) {
|
|
7397
|
+
return { ok: false, reason: `anchor position out of range (fromEnd=${anchor.fromEnd}, have ${roleIdx.length})` };
|
|
7398
|
+
}
|
|
7399
|
+
const index = roleIdx[pos];
|
|
7400
|
+
const got = normalizeText(recordText(lines[index].obj, anchor.role));
|
|
7401
|
+
const want = normalizeText(anchor.oldText);
|
|
7402
|
+
if (got !== want) {
|
|
7403
|
+
if (!(want.length > 0 && got.startsWith(want))) {
|
|
7404
|
+
return { ok: false, reason: "anchor text mismatch \u2014 stores out of sync, refusing to edit" };
|
|
7405
|
+
}
|
|
7406
|
+
}
|
|
7407
|
+
return { ok: true, index };
|
|
7408
|
+
}
|
|
7409
|
+
function orphanCheckObjs(objs) {
|
|
7410
|
+
const toolUseIds = /* @__PURE__ */ new Set();
|
|
7411
|
+
const toolResultIds = /* @__PURE__ */ new Set();
|
|
7412
|
+
for (const obj of objs) {
|
|
7413
|
+
const content = obj?.message?.content;
|
|
7414
|
+
if (!Array.isArray(content)) continue;
|
|
7415
|
+
for (const b of content) {
|
|
7416
|
+
if (b?.type === "tool_use" && b.id) toolUseIds.add(b.id);
|
|
7417
|
+
if (b?.type === "tool_result" && b.tool_use_id) toolResultIds.add(b.tool_use_id);
|
|
7418
|
+
}
|
|
7419
|
+
}
|
|
7420
|
+
for (const id of toolUseIds) {
|
|
7421
|
+
if (!toolResultIds.has(id)) return true;
|
|
7422
|
+
}
|
|
7423
|
+
return false;
|
|
7424
|
+
}
|
|
7425
|
+
function applyTranscriptEdit(file, index, newText) {
|
|
7426
|
+
if (!existsSync$1(file)) return { ok: false, reason: `transcript not found: ${file}` };
|
|
7427
|
+
const lines = parseTranscript(file);
|
|
7428
|
+
if (index < 0 || index >= lines.length) return { ok: false, reason: "index out of range" };
|
|
7429
|
+
const target = lines[index].obj;
|
|
7430
|
+
if (!target) return { ok: false, reason: "target line not parseable" };
|
|
7431
|
+
if (target.type === "assistant") {
|
|
7432
|
+
const content = target?.message?.content;
|
|
7433
|
+
if (!Array.isArray(content)) return { ok: false, reason: "assistant content is not a block array" };
|
|
7434
|
+
const textBlocks = content.filter((b) => b && b.type === "text" && typeof b.text === "string");
|
|
7435
|
+
if (textBlocks.length !== 1) {
|
|
7436
|
+
return { ok: false, reason: `expected exactly 1 text block, found ${textBlocks.length}` };
|
|
7437
|
+
}
|
|
7438
|
+
textBlocks[0].text = newText;
|
|
7439
|
+
} else if (target.type === "user") {
|
|
7440
|
+
if (typeof target?.message?.content !== "string") {
|
|
7441
|
+
return { ok: false, reason: "user content is not a string" };
|
|
7442
|
+
}
|
|
7443
|
+
target.message.content = newText;
|
|
7444
|
+
} else {
|
|
7445
|
+
return { ok: false, reason: `unsupported record type: ${target.type}` };
|
|
7446
|
+
}
|
|
7447
|
+
const keptObjs = [];
|
|
7448
|
+
const kept = [];
|
|
7449
|
+
for (let i = 0; i <= index; i++) {
|
|
7450
|
+
kept.push(i === index ? JSON.stringify(target) : lines[i].raw);
|
|
7451
|
+
keptObjs.push(lines[i].obj);
|
|
7452
|
+
}
|
|
7453
|
+
if (orphanCheckObjs(keptObjs)) {
|
|
7454
|
+
return { ok: false, reason: "edit would orphan a tool_use (truncation drops its tool_result)" };
|
|
7455
|
+
}
|
|
7456
|
+
const out = kept.join("\n") + "\n";
|
|
7457
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
7458
|
+
try {
|
|
7459
|
+
writeFileSync$1(tmp, out);
|
|
7460
|
+
renameSync$1(tmp, file);
|
|
7461
|
+
} catch (err) {
|
|
7462
|
+
return { ok: false, reason: `write failed: ${err?.message ?? err}` };
|
|
7463
|
+
}
|
|
7464
|
+
return { ok: true, truncatedAfter: index, totalBefore: lines.length };
|
|
7465
|
+
}
|
|
7466
|
+
function saveUndoSnapshot(undoDir, transcriptFile, messagesFile, meta) {
|
|
7467
|
+
mkdirSync$1(undoDir, { recursive: true });
|
|
7468
|
+
if (existsSync$1(transcriptFile)) copyFileSync(transcriptFile, join$1(undoDir, "transcript.jsonl"));
|
|
7469
|
+
if (existsSync$1(messagesFile)) copyFileSync(messagesFile, join$1(undoDir, "messages.jsonl"));
|
|
7470
|
+
writeFileSync$1(join$1(undoDir, "meta.json"), JSON.stringify(meta));
|
|
7471
|
+
}
|
|
7472
|
+
function readUndoMeta(undoDir) {
|
|
7473
|
+
const p = join$1(undoDir, "meta.json");
|
|
7474
|
+
if (!existsSync$1(p)) return null;
|
|
7475
|
+
try {
|
|
7476
|
+
return JSON.parse(readFileSync$1(p, "utf-8"));
|
|
7477
|
+
} catch {
|
|
7478
|
+
return null;
|
|
7479
|
+
}
|
|
7480
|
+
}
|
|
7481
|
+
function restoreTranscriptFromUndo(undoDir, targetFile) {
|
|
7482
|
+
const backup = join$1(undoDir, "transcript.jsonl");
|
|
7483
|
+
if (!existsSync$1(backup)) return false;
|
|
7484
|
+
const tmp = `${targetFile}.tmp-${process.pid}`;
|
|
7485
|
+
writeFileSync$1(tmp, readFileSync$1(backup));
|
|
7486
|
+
renameSync$1(tmp, targetFile);
|
|
7487
|
+
return true;
|
|
7488
|
+
}
|
|
7489
|
+
function clearUndoSnapshot(undoDir) {
|
|
7490
|
+
try {
|
|
7491
|
+
rmSync$1(undoDir, { recursive: true, force: true });
|
|
7492
|
+
} catch {
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
function transcriptSessionIdMatches(file, expectedSessionId) {
|
|
7496
|
+
if (!existsSync$1(file)) return false;
|
|
7497
|
+
const lines = parseTranscript(file);
|
|
7498
|
+
for (const { obj } of lines) {
|
|
7499
|
+
if (obj && typeof obj.sessionId === "string") {
|
|
7500
|
+
return obj.sessionId === expectedSessionId;
|
|
7501
|
+
}
|
|
7502
|
+
}
|
|
7503
|
+
return false;
|
|
7504
|
+
}
|
|
7505
|
+
|
|
7152
7506
|
const execFileAsync = promisify$1(execFile$1);
|
|
7153
7507
|
const SVAMP_TOOLS_DIR = join(homedir(), ".svamp", "tools");
|
|
7154
7508
|
const SVAMP_BIN_DIR = join(SVAMP_TOOLS_DIR, "bin");
|
|
@@ -10451,7 +10805,7 @@ async function startDaemon(options) {
|
|
|
10451
10805
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
10452
10806
|
saveExposedTunnels(list);
|
|
10453
10807
|
}
|
|
10454
|
-
const { ServeManager } = await import('./serveManager-
|
|
10808
|
+
const { ServeManager } = await import('./serveManager-BnwAe-VX.mjs');
|
|
10455
10809
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
10456
10810
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
10457
10811
|
});
|
|
@@ -11503,12 +11857,13 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11503
11857
|
}
|
|
11504
11858
|
return child;
|
|
11505
11859
|
};
|
|
11506
|
-
const restartClaudeHandler = async () => {
|
|
11860
|
+
const restartClaudeHandler = async (opts) => {
|
|
11507
11861
|
logger.log(`[Session ${sessionId}] Restart Claude requested`);
|
|
11508
11862
|
if (isRestartingClaude || isSwitchingMode) {
|
|
11509
11863
|
return { success: false, message: "Restart already in progress." };
|
|
11510
11864
|
}
|
|
11511
11865
|
isRestartingClaude = true;
|
|
11866
|
+
let beforeRespawnError;
|
|
11512
11867
|
try {
|
|
11513
11868
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
11514
11869
|
isKillingClaude = true;
|
|
@@ -11520,6 +11875,14 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11520
11875
|
if (trackedSession?.stopped) {
|
|
11521
11876
|
return { success: false, message: "Session was stopped during restart." };
|
|
11522
11877
|
}
|
|
11878
|
+
if (opts?.beforeRespawn) {
|
|
11879
|
+
try {
|
|
11880
|
+
await opts.beforeRespawn();
|
|
11881
|
+
} catch (hookErr) {
|
|
11882
|
+
beforeRespawnError = hookErr?.message ?? String(hookErr);
|
|
11883
|
+
logger.log(`[Session ${sessionId}] beforeRespawn hook failed: ${beforeRespawnError}`);
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
11523
11886
|
if (claudeResumeId) {
|
|
11524
11887
|
if (!stagedCredentials && shouldIsolateSession()) {
|
|
11525
11888
|
try {
|
|
@@ -11532,6 +11895,9 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11532
11895
|
spawnClaude(void 0, { permissionMode: currentPermissionMode });
|
|
11533
11896
|
sessionService.updateMetadata(sessionMetadata);
|
|
11534
11897
|
logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
|
|
11898
|
+
if (beforeRespawnError) {
|
|
11899
|
+
return { success: false, message: `Edit failed (session restored): ${beforeRespawnError}` };
|
|
11900
|
+
}
|
|
11535
11901
|
return { success: true, message: "Claude process restarted successfully." };
|
|
11536
11902
|
} else {
|
|
11537
11903
|
logger.log(`[Session ${sessionId}] No resume ID \u2014 cannot restart`);
|
|
@@ -11545,6 +11911,26 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11545
11911
|
isRestartingClaude = false;
|
|
11546
11912
|
}
|
|
11547
11913
|
};
|
|
11914
|
+
const editHistoryIdleGuard = () => {
|
|
11915
|
+
const lifecycle = sessionMetadata.lifecycleState;
|
|
11916
|
+
if (lifecycle === "running" || lifecycle === "restarting") {
|
|
11917
|
+
return { success: false, message: "Cannot edit while the agent is working \u2014 wait until it is idle." };
|
|
11918
|
+
}
|
|
11919
|
+
if (isRestartingClaude || isSwitchingMode || isKillingClaude) {
|
|
11920
|
+
return { success: false, message: "Cannot edit during a restart/mode switch \u2014 try again in a moment." };
|
|
11921
|
+
}
|
|
11922
|
+
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
11923
|
+
if (queueLen > 0) {
|
|
11924
|
+
return { success: false, message: "Cannot edit while messages are queued." };
|
|
11925
|
+
}
|
|
11926
|
+
if (isLoopActiveForSession(directory, sessionId)) {
|
|
11927
|
+
return { success: false, message: "Cannot edit history while a loop is active." };
|
|
11928
|
+
}
|
|
11929
|
+
if (!claudeResumeId) {
|
|
11930
|
+
return { success: false, message: "No Claude transcript to edit yet." };
|
|
11931
|
+
}
|
|
11932
|
+
return null;
|
|
11933
|
+
};
|
|
11548
11934
|
if (shouldIsolateSession()) {
|
|
11549
11935
|
try {
|
|
11550
11936
|
stagedCredentials = await stageCredentialsForSharing(sessionId);
|
|
@@ -11565,6 +11951,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11565
11951
|
logger.log(`[Session ${sessionId}] User message received`);
|
|
11566
11952
|
userMessagePending = true;
|
|
11567
11953
|
turnInitiatedByUser = true;
|
|
11954
|
+
clearUndoSnapshot(join$1(getSessionDir(directory, sessionId), "undo"));
|
|
11568
11955
|
let text;
|
|
11569
11956
|
let msgMeta = meta;
|
|
11570
11957
|
try {
|
|
@@ -11784,6 +12171,86 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11784
12171
|
}
|
|
11785
12172
|
},
|
|
11786
12173
|
onRestartClaude: restartClaudeHandler,
|
|
12174
|
+
onEditTranscript: async ({ role, fromEnd, oldText, newText, applyStoreA }) => {
|
|
12175
|
+
const gate = editHistoryIdleGuard();
|
|
12176
|
+
if (gate) return gate;
|
|
12177
|
+
logger.log(`[Session ${sessionId}] Edit history: role=${role} fromEnd=${fromEnd} \u2192 kill + rewrite + resume`);
|
|
12178
|
+
const result = await restartClaudeHandler({
|
|
12179
|
+
beforeRespawn: async () => {
|
|
12180
|
+
const file = resolveTranscriptPath(directory, claudeResumeId);
|
|
12181
|
+
if (!transcriptSessionIdMatches(file, claudeResumeId)) {
|
|
12182
|
+
throw new Error("transcript file id mismatch (session rotated) \u2014 aborting edit");
|
|
12183
|
+
}
|
|
12184
|
+
const lines = parseTranscript(file);
|
|
12185
|
+
const anchor = findAnchorIndex(lines, { role, fromEnd, oldText });
|
|
12186
|
+
if (!anchor.ok) throw new Error(anchor.reason);
|
|
12187
|
+
const sessDir = getSessionDir(directory, sessionId);
|
|
12188
|
+
saveUndoSnapshot(join$1(sessDir, "undo"), file, join$1(sessDir, "messages.jsonl"), {
|
|
12189
|
+
preEditClaudeSessionId: claudeResumeId,
|
|
12190
|
+
createdAt: Date.now(),
|
|
12191
|
+
label: "edit"
|
|
12192
|
+
});
|
|
12193
|
+
const edit = applyTranscriptEdit(file, anchor.index, newText);
|
|
12194
|
+
if (!edit.ok) throw new Error(edit.reason);
|
|
12195
|
+
applyStoreA();
|
|
12196
|
+
logger.log(`[Session ${sessionId}] Edit applied \u2014 transcript truncated after line ${anchor.index}`);
|
|
12197
|
+
}
|
|
12198
|
+
});
|
|
12199
|
+
if (result.success && claudeResumeId && !trackedSession.stopped) {
|
|
12200
|
+
saveSession({
|
|
12201
|
+
sessionId,
|
|
12202
|
+
directory,
|
|
12203
|
+
claudeResumeId,
|
|
12204
|
+
permissionMode: currentPermissionMode,
|
|
12205
|
+
spawnMeta: lastSpawnMeta,
|
|
12206
|
+
metadata: sessionMetadata,
|
|
12207
|
+
createdAt: sessionCreatedAt,
|
|
12208
|
+
machineId,
|
|
12209
|
+
wasProcessing: false
|
|
12210
|
+
});
|
|
12211
|
+
artifactSync.syncSession(sessionId, getSessionDir(directory, sessionId), sessionMetadata, machineId).catch(() => {
|
|
12212
|
+
});
|
|
12213
|
+
}
|
|
12214
|
+
return result;
|
|
12215
|
+
},
|
|
12216
|
+
onUndoEdit: async ({ restoreStoreA }) => {
|
|
12217
|
+
const gate = editHistoryIdleGuard();
|
|
12218
|
+
if (gate) return gate;
|
|
12219
|
+
const undoDir = join$1(getSessionDir(directory, sessionId), "undo");
|
|
12220
|
+
const meta = readUndoMeta(undoDir);
|
|
12221
|
+
if (!meta) return { success: false, message: "Nothing to undo." };
|
|
12222
|
+
logger.log(`[Session ${sessionId}] Undo edit \u2192 restore pre-edit transcript ${meta.preEditClaudeSessionId} + messages, resume`);
|
|
12223
|
+
const result = await restartClaudeHandler({
|
|
12224
|
+
beforeRespawn: async () => {
|
|
12225
|
+
const target = resolveTranscriptPath(directory, meta.preEditClaudeSessionId);
|
|
12226
|
+
if (!restoreTranscriptFromUndo(undoDir, target)) {
|
|
12227
|
+
throw new Error("undo snapshot missing transcript backup");
|
|
12228
|
+
}
|
|
12229
|
+
restoreStoreA();
|
|
12230
|
+
claudeResumeId = meta.preEditClaudeSessionId;
|
|
12231
|
+
sessionMetadata = { ...sessionMetadata, claudeSessionId: claudeResumeId };
|
|
12232
|
+
}
|
|
12233
|
+
});
|
|
12234
|
+
if (result.success) {
|
|
12235
|
+
clearUndoSnapshot(undoDir);
|
|
12236
|
+
if (claudeResumeId && !trackedSession.stopped) {
|
|
12237
|
+
saveSession({
|
|
12238
|
+
sessionId,
|
|
12239
|
+
directory,
|
|
12240
|
+
claudeResumeId,
|
|
12241
|
+
permissionMode: currentPermissionMode,
|
|
12242
|
+
spawnMeta: lastSpawnMeta,
|
|
12243
|
+
metadata: sessionMetadata,
|
|
12244
|
+
createdAt: sessionCreatedAt,
|
|
12245
|
+
machineId,
|
|
12246
|
+
wasProcessing: false
|
|
12247
|
+
});
|
|
12248
|
+
artifactSync.syncSession(sessionId, getSessionDir(directory, sessionId), sessionMetadata, machineId).catch(() => {
|
|
12249
|
+
});
|
|
12250
|
+
}
|
|
12251
|
+
}
|
|
12252
|
+
return result;
|
|
12253
|
+
},
|
|
11787
12254
|
onUpdateSecurityContext: async (newSecurityContext) => {
|
|
11788
12255
|
logger.log(`[Session ${sessionId}] Security context update requested \u2014 restarting agent`);
|
|
11789
12256
|
sessionMetadata = { ...sessionMetadata, securityContext: newSecurityContext };
|
|
@@ -12936,7 +13403,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12936
13403
|
const specs = loadExposedTunnels();
|
|
12937
13404
|
if (specs.length === 0) return;
|
|
12938
13405
|
logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
|
|
12939
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
13406
|
+
const { FrpcTunnel } = await import('./frpc-1aSnPVrE.mjs');
|
|
12940
13407
|
for (const spec of specs) {
|
|
12941
13408
|
if (tunnels.has(spec.name)) continue;
|
|
12942
13409
|
try {
|