@monster-orange/den 0.4.21 → 0.4.23
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/CHANGELOG.md +115 -0
- package/docs/distributions.md +186 -42
- package/package.json +1 -1
- package/service/install.sh +213 -63
- package/src/shared/agents/den-claude.ts +13 -1
- package/src/shared/agents/guardrails/rotation-hook.ts +5 -0
- package/src/shared/agents/records.ts +6 -0
- package/src/shared/agents/rotate-settings.ts +309 -0
- package/src/shared/agents/rotation.ts +110 -5
- package/src/shared/agents/runtime.ts +6 -0
- package/src/shared/agents/session-name.ts +21 -1
- package/src/shared/den-config.ts +27 -12
- package/src/shared/launchd-session.ts +92 -0
- package/src/shared/sessions/client.ts +24 -0
- package/src/shared/sessions/host.ts +114 -5
- package/src/shared/sessions/types.ts +5 -0
- package/src/ui/cli/agent-ops.ts +469 -17
- package/src/ui/cli/agents-command.ts +200 -3
- package/src/ui/cli/doctor-command.ts +72 -0
- package/src/ui/cli/doctor-where.ts +203 -0
- package/src/ui/cli/index.ts +1 -1
- package/src/ui/cli/server-agent-ops.ts +2 -2
- package/src/ui/cli/service-command.ts +217 -11
- package/src/ui/cli/sessions-command.ts +46 -12
- package/src/ui/cli/web-command.ts +36 -5
- package/src/ui/cli/web-service.ts +160 -0
- package/src/ui/server/agent-routes.ts +149 -5
- package/src/ui/server/index.ts +17 -5
- package/src/ui/server/public/agents-page.js +316 -27
- package/src/ui/server/public/agents.html +62 -0
- package/src/ui/server/public/i18n/es.js +58 -2
- package/src/ui/server/public/i18n/ja.js +54 -2
- package/src/ui/server/public/settings-registry.js +204 -0
- package/src/ui/server/public/settings.html +43 -4
- package/src/ui/server/public/shell.js +56 -2
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/session-routes.ts +27 -5
|
@@ -99,8 +99,10 @@ import {
|
|
|
99
99
|
import {
|
|
100
100
|
readWorkspacesFile,
|
|
101
101
|
conversationFolders,
|
|
102
|
+
listWorkspaceServices,
|
|
102
103
|
resolveWorkspaceDir,
|
|
103
104
|
} from "../../shared/environment-manager/workspaces.js";
|
|
105
|
+
import { readCatalog } from "../../shared/environment-manager/catalog.js";
|
|
104
106
|
import { getStateDir } from "../../shared/paths.js";
|
|
105
107
|
import { HostError } from "../../shared/sessions/client.js";
|
|
106
108
|
import {
|
|
@@ -133,7 +135,13 @@ import {
|
|
|
133
135
|
changePersona,
|
|
134
136
|
listConversations,
|
|
135
137
|
relaunchAgent,
|
|
136
|
-
|
|
138
|
+
resumeOrRelaunch,
|
|
139
|
+
keepAgentConversation,
|
|
140
|
+
askForHandoff,
|
|
141
|
+
rotationSettings,
|
|
142
|
+
setRotationSetting,
|
|
143
|
+
rotationTick,
|
|
144
|
+
ROTATION_TICK_MS,
|
|
137
145
|
startAgent,
|
|
138
146
|
startFresh,
|
|
139
147
|
stopAgent,
|
|
@@ -167,7 +175,7 @@ export interface AgentRouteDeps {
|
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
const AGENTS_PATH =
|
|
170
|
-
/^\/api\/workspaces\/([^/]+)\/agents(?:\/([^/]+)\/(stop|resume|fresh|relaunch|remove|name|paths|persona))?$/;
|
|
178
|
+
/^\/api\/workspaces\/([^/]+)\/agents(?:\/([^/]+)\/(stop|resume|fresh|relaunch|remove|name|paths|persona|keep|handoff))?$/;
|
|
171
179
|
|
|
172
180
|
const RUNTIME_PATH = /^\/api\/agents\/runtimes\/([^/]+)\/path$/;
|
|
173
181
|
const PERSONA_PATH = /^\/api\/personas\/([^/]+)$/;
|
|
@@ -203,6 +211,15 @@ export function webDeps(over: AgentsDeps = {}): AgentsResolved {
|
|
|
203
211
|
return conversationFolders(ws, cfg, getReposRoot());
|
|
204
212
|
},
|
|
205
213
|
}),
|
|
214
|
+
// D28: the service scope of rotation settings.
|
|
215
|
+
workspaceServices: (ws) => {
|
|
216
|
+
const cfg = readWorkspacesFile().workspaces[ws];
|
|
217
|
+
if (!cfg) return [];
|
|
218
|
+
return listWorkspaceServices(cfg, readCatalog(), ws).map((r) => ({
|
|
219
|
+
address: r.address,
|
|
220
|
+
path: r.repoPath,
|
|
221
|
+
}));
|
|
222
|
+
},
|
|
206
223
|
stdout: NO_TTY,
|
|
207
224
|
out: () => undefined,
|
|
208
225
|
err: () => undefined,
|
|
@@ -406,6 +423,8 @@ export async function handleAgentRoutes(
|
|
|
406
423
|
pathname === "/api/agents/clear-stopped" ||
|
|
407
424
|
pathname === "/api/agents/restore" ||
|
|
408
425
|
pathname === "/api/agents/resume-interrupted";
|
|
426
|
+
if (pathname === "/api/agents/rotation")
|
|
427
|
+
return rotationRoute(req, res, webDeps(deps.agents));
|
|
409
428
|
if (
|
|
410
429
|
pathname !== "/api/personas" &&
|
|
411
430
|
pathname !== "/api/agents/runtimes" &&
|
|
@@ -490,6 +509,8 @@ export async function handleAgentRoutes(
|
|
|
490
509
|
| "name"
|
|
491
510
|
| "paths"
|
|
492
511
|
| "persona"
|
|
512
|
+
| "keep"
|
|
513
|
+
| "handoff"
|
|
493
514
|
| undefined;
|
|
494
515
|
|
|
495
516
|
if (!action && method === "GET") {
|
|
@@ -571,6 +592,12 @@ export async function handleAgentRoutes(
|
|
|
571
592
|
"only a person can change an agent's persona (an agent can't change its own or another's)",
|
|
572
593
|
{ status: 403 },
|
|
573
594
|
);
|
|
595
|
+
// D28: an agent could keep itself from ever rotating.
|
|
596
|
+
if (verb === "keep" && (agent || actor))
|
|
597
|
+
throw new AgentError(
|
|
598
|
+
"only a person can keep an agent's conversation past its rotation",
|
|
599
|
+
{ status: 403 },
|
|
600
|
+
);
|
|
574
601
|
if (verb === "resume" && agent) {
|
|
575
602
|
const why = agentResumeRefusal(agent, ws, id!, mcpActions());
|
|
576
603
|
if (why) throw new AgentError(why, { status: 403 });
|
|
@@ -651,6 +678,38 @@ export async function handleAgentRoutes(
|
|
|
651
678
|
what = `start ${s.rec.id} fresh (${s.rec.persona}, ${s.rec.runtime}) in ${ws}`;
|
|
652
679
|
audit(what, s.early ? "exited at once" : undefined);
|
|
653
680
|
sendStarted(res, s, d);
|
|
681
|
+
} else if (verb === "keep") {
|
|
682
|
+
const body = await jsonBody(req);
|
|
683
|
+
for (const k of Object.keys(body))
|
|
684
|
+
if (k !== "never")
|
|
685
|
+
throw new AgentError(`${k}: unknown field (keep takes never)`, {
|
|
686
|
+
usage: true,
|
|
687
|
+
});
|
|
688
|
+
if (body.never !== undefined && typeof body.never !== "boolean")
|
|
689
|
+
throw new AgentError("never must be true or false", { usage: true });
|
|
690
|
+
const rec = keepAgentConversation(d, id!, ws, {
|
|
691
|
+
never: body.never === true,
|
|
692
|
+
});
|
|
693
|
+
what = `keep ${rec.id}'s conversation in ${ws}${body.never ? " (never rotate)" : ""}`;
|
|
694
|
+
audit(what);
|
|
695
|
+
sendJson(res, { ok: true, agent: rec.id, never: body.never === true });
|
|
696
|
+
} else if (verb === "handoff") {
|
|
697
|
+
const body = await jsonBody(req);
|
|
698
|
+
for (const k of Object.keys(body))
|
|
699
|
+
if (k !== "relaunch")
|
|
700
|
+
throw new AgentError(`${k}: unknown field (handoff takes relaunch)`, {
|
|
701
|
+
usage: true,
|
|
702
|
+
});
|
|
703
|
+
const relaunch = body.relaunch === true;
|
|
704
|
+
const rec = await askForHandoff(d, id!, ws, undefined, { relaunch });
|
|
705
|
+
what = `ask ${rec.id} for its handoff in ${ws}${relaunch ? ", then relaunch" : ""}${rec.resumed ? " (resumed to write it)" : ""}`;
|
|
706
|
+
audit(what);
|
|
707
|
+
sendJson(res, {
|
|
708
|
+
ok: true,
|
|
709
|
+
agent: rec.id,
|
|
710
|
+
relaunch,
|
|
711
|
+
...(rec.resumed ? { resumed: true } : {}),
|
|
712
|
+
});
|
|
654
713
|
} else if (verb === "relaunch") {
|
|
655
714
|
const s = await relaunchAgent(d, id!, ws);
|
|
656
715
|
what = `relaunch ${s.rec.id} from its handoff (${s.why}) in ${ws}`;
|
|
@@ -732,9 +791,16 @@ export async function handleAgentRoutes(
|
|
|
732
791
|
audit(what);
|
|
733
792
|
sendJson(res, { ok: true, removed });
|
|
734
793
|
} else {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
794
|
+
// D28: a due agent under auto relaunches from its handoff instead.
|
|
795
|
+
const r = await resumeOrRelaunch(d, id!, ws);
|
|
796
|
+
if (r.kind === "relaunched") {
|
|
797
|
+
what = `relaunch ${r.s.rec.id} from its handoff (${r.s.why}) in ${ws}, on Resume`;
|
|
798
|
+
audit(what, r.s.early ? "exited at once" : undefined);
|
|
799
|
+
sendStarted(res, r.s, d, { relaunched: true });
|
|
800
|
+
} else {
|
|
801
|
+
audit(what);
|
|
802
|
+
sendStarted(res, r.s, d);
|
|
803
|
+
}
|
|
738
804
|
}
|
|
739
805
|
} catch (e) {
|
|
740
806
|
audit(what, e instanceof Error ? e.message : String(e));
|
|
@@ -1209,8 +1275,86 @@ export function startAgentUpkeep(
|
|
|
1209
1275
|
log(` ⚠ agents.remove_stopped_after: ${(e as Error).message}`);
|
|
1210
1276
|
}
|
|
1211
1277
|
};
|
|
1278
|
+
// D28: due agents under on_due auto, at idle: asked for the handoff,
|
|
1279
|
+
// then relaunched from it. One look at a time.
|
|
1280
|
+
let looking = false;
|
|
1281
|
+
const rotate = async () => {
|
|
1282
|
+
if (looking) return;
|
|
1283
|
+
looking = true;
|
|
1284
|
+
try {
|
|
1285
|
+
for (const e of await rotationTick(d)) {
|
|
1286
|
+
const who = `${e.agent} in ${e.workspace}`;
|
|
1287
|
+
if (e.kind === "relaunched") {
|
|
1288
|
+
const what = `relaunch ${who} from its handoff (${e.why}), by itself; the old conversation ${e.old ?? "?"} is kept`;
|
|
1289
|
+
recordAction(null, { verb: "agents", detail: what });
|
|
1290
|
+
log(`Relaunched ${who} from its handoff after ${e.why}`);
|
|
1291
|
+
} else if (e.kind === "asked")
|
|
1292
|
+
log(`Asked ${who} for its handoff (${e.asked} of 2)`);
|
|
1293
|
+
else if (e.kind === "fell-back")
|
|
1294
|
+
log(
|
|
1295
|
+
` ⚠ ${who} wrote no handoff after 2 asks: its callout asks the person`,
|
|
1296
|
+
);
|
|
1297
|
+
else log(` ⚠ rotation of ${who}: ${e.why}`);
|
|
1298
|
+
}
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
log(` ⚠ agents.rotate: ${(e as Error).message}`);
|
|
1301
|
+
} finally {
|
|
1302
|
+
looking = false;
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1212
1305
|
setTimeout(() => {
|
|
1213
1306
|
void restart().then(prune);
|
|
1214
1307
|
}, deps.delayMs ?? 2000).unref();
|
|
1215
1308
|
setInterval(() => void prune(), PRUNE_EVERY_MS).unref();
|
|
1309
|
+
setInterval(() => void rotate(), ROTATION_TICK_MS).unref();
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* D28, `/api/agents/rotation`. GET: the settings (install, overrides,
|
|
1314
|
+
* defaults) and, with `?ws=&agent=`, that agent's effective values and
|
|
1315
|
+
* where each comes from. PUT `{ scope, target?, key, value }` sets one
|
|
1316
|
+
* (value null clears it): a person's only.
|
|
1317
|
+
*/
|
|
1318
|
+
async function rotationRoute(
|
|
1319
|
+
req: http.IncomingMessage,
|
|
1320
|
+
res: http.ServerResponse,
|
|
1321
|
+
d: AgentsResolved,
|
|
1322
|
+
): Promise<boolean> {
|
|
1323
|
+
const method = req.method ?? "GET";
|
|
1324
|
+
try {
|
|
1325
|
+
if (method === "GET" || method === "HEAD") {
|
|
1326
|
+
const q = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
1327
|
+
const ws = q.get("ws") ?? undefined;
|
|
1328
|
+
const agent = q.get("agent") ?? undefined;
|
|
1329
|
+
if ((ws && !NAME_RE.test(ws)) || (agent && !NAME_RE.test(agent)))
|
|
1330
|
+
throw new AgentError("not a name", { usage: true });
|
|
1331
|
+
sendJson(res, rotationSettings(d, agent, ws));
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
if (method !== "PUT") return methodNotAllowed(res, "GET, PUT");
|
|
1335
|
+
if (agentPrincipalOf(req) || auditActorOf(req))
|
|
1336
|
+
throw new AgentError("only a person can change rotation settings", {
|
|
1337
|
+
status: 403,
|
|
1338
|
+
});
|
|
1339
|
+
const body = await jsonBody(req);
|
|
1340
|
+
for (const k of Object.keys(body))
|
|
1341
|
+
if (!["scope", "target", "key", "value"].includes(k))
|
|
1342
|
+
throw new AgentError(
|
|
1343
|
+
`${k}: unknown field (takes scope, target, key, value)`,
|
|
1344
|
+
{ usage: true },
|
|
1345
|
+
);
|
|
1346
|
+
const out = setRotationSetting(d, body);
|
|
1347
|
+
recordAction(
|
|
1348
|
+
null,
|
|
1349
|
+
{
|
|
1350
|
+
verb: "agents",
|
|
1351
|
+
detail: `rotation ${String(body.scope)}${body.target ? ` ${String(body.target)}` : ""} ${String(body.key)} = ${body.value === null ? "(unset)" : JSON.stringify(body.value)}`,
|
|
1352
|
+
},
|
|
1353
|
+
principalOf(req),
|
|
1354
|
+
);
|
|
1355
|
+
sendJson(res, { ok: true, ...out });
|
|
1356
|
+
} catch (e) {
|
|
1357
|
+
sendError(res, e);
|
|
1358
|
+
}
|
|
1359
|
+
return true;
|
|
1216
1360
|
}
|
package/src/ui/server/index.ts
CHANGED
|
@@ -868,7 +868,12 @@ import {
|
|
|
868
868
|
import { heldActions } from "./held-actions.js";
|
|
869
869
|
import type { ApprovalQueue } from "./approvals.js";
|
|
870
870
|
import { processPathRules } from "../cli/onboard-reads.js";
|
|
871
|
-
import {
|
|
871
|
+
import {
|
|
872
|
+
handleAgentRoutes,
|
|
873
|
+
startAgentUpkeep,
|
|
874
|
+
webDeps as agentWebDeps,
|
|
875
|
+
} from "./agent-routes.js";
|
|
876
|
+
import { policyResolver } from "../cli/agent-ops.js";
|
|
872
877
|
import { handleServerAgentRoutes } from "./server-agent-routes.js";
|
|
873
878
|
import { freshDen } from "../cli/server-agent-ops.js";
|
|
874
879
|
import {
|
|
@@ -918,6 +923,7 @@ import {
|
|
|
918
923
|
|
|
919
924
|
import { product } from "../../shared/product.js";
|
|
920
925
|
import { readBrand } from "../../shared/brand.js";
|
|
926
|
+
import { launchdSession } from "../../shared/launchd-session.js";
|
|
921
927
|
/** config `web.port` (user > team), falling back to the built-in default. */
|
|
922
928
|
function readWebPortSafe(): number {
|
|
923
929
|
try {
|
|
@@ -4422,6 +4428,9 @@ async function handleRequest(
|
|
|
4422
4428
|
// another distribution's server (see cli/server-identity.ts).
|
|
4423
4429
|
root: ROOT,
|
|
4424
4430
|
product: product().name,
|
|
4431
|
+
// D20: the launchd session it runs in (Aqua ends at logout); doctor
|
|
4432
|
+
// shows it. null off macOS.
|
|
4433
|
+
launchd: launchdSession(),
|
|
4425
4434
|
startedAt: new Date(SERVER_STARTED_AT).toISOString(),
|
|
4426
4435
|
uptimeMs: Date.now() - SERVER_STARTED_AT,
|
|
4427
4436
|
stale,
|
|
@@ -4744,7 +4753,10 @@ async function handleRequest(
|
|
|
4744
4753
|
res,
|
|
4745
4754
|
pathname,
|
|
4746
4755
|
guard.ok && (!guard.beyond || !!guard.session || !!guard.principal),
|
|
4747
|
-
{
|
|
4756
|
+
{
|
|
4757
|
+
waitingAgents: waitingAgentIds,
|
|
4758
|
+
rotationPolicy: () => policyResolver(agentWebDeps()),
|
|
4759
|
+
},
|
|
4748
4760
|
)
|
|
4749
4761
|
)
|
|
4750
4762
|
return;
|
|
@@ -5575,9 +5587,9 @@ server.on("error", (err: NodeJS.ErrnoException) => {
|
|
|
5575
5587
|
if (err.code === "EADDRINUSE") {
|
|
5576
5588
|
console.error(
|
|
5577
5589
|
`\n ✗ Port ${PORT} is already in use.\n` +
|
|
5578
|
-
`\n Another ${product().cli}
|
|
5579
|
-
`\n
|
|
5580
|
-
`\n
|
|
5590
|
+
`\n Another ${product().cli} server may be running, or its service holds the port.` +
|
|
5591
|
+
`\n ${product().cli} service status # is the service running it?` +
|
|
5592
|
+
`\n ${product().cli} web restart # restarts the service, or the plain server\n`,
|
|
5581
5593
|
);
|
|
5582
5594
|
process.exit(1);
|
|
5583
5595
|
}
|