@monster-orange/den 0.4.22 → 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 +47 -0
- package/docs/distributions.md +91 -30
- package/package.json +1 -1
- package/src/shared/agents/guardrails/rotation-hook.ts +5 -0
- package/src/shared/agents/rotate-settings.ts +309 -0
- package/src/shared/agents/rotation.ts +110 -5
- package/src/shared/den-config.ts +27 -12
- package/src/ui/cli/agent-ops.ts +449 -15
- package/src/ui/cli/agents-command.ts +169 -3
- package/src/ui/cli/server-agent-ops.ts +2 -2
- package/src/ui/cli/web-command.ts +32 -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 +13 -5
- package/src/ui/server/public/agents-page.js +214 -22
- package/src/ui/server/public/agents.html +32 -0
- package/src/ui/server/public/i18n/es.js +44 -2
- package/src/ui/server/public/i18n/ja.js +40 -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 +42 -2
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/session-routes.ts +23 -5
|
@@ -487,9 +487,50 @@
|
|
|
487
487
|
"Su sesión rotó ({why}), pero no escribió handoff.md. Reanúdalo para que lo escriba, o empieza de cero.",
|
|
488
488
|
"ag.rot.badHandoff":
|
|
489
489
|
"Su sesión rotó ({why}), pero su handoff.md no está completo. Reanúdalo para arreglarlo, o empieza de cero.",
|
|
490
|
-
"ag.relaunch": "Relanzar
|
|
491
|
-
"ag.relaunchName": "Relanzar {id}
|
|
490
|
+
"ag.relaunch": "Relanzar ahora",
|
|
491
|
+
"ag.relaunchName": "Relanzar {id} ahora",
|
|
492
492
|
"ag.relaunched": "{id} relanzado desde su traspaso",
|
|
493
|
+
"ag.rc.autoAsk":
|
|
494
|
+
"Toca rotar ({why}). Den le pide el traspaso cuando esté libre y luego lo relanza.",
|
|
495
|
+
"ag.rc.autoReady":
|
|
496
|
+
"Toca rotar ({why}). Su traspaso está listo: Den lo relanza cuando esté libre.",
|
|
497
|
+
"ag.rc.ready": "Toca rotar ({why}). Su traspaso está listo.",
|
|
498
|
+
"ag.rc.missing": "Toca rotar ({why}). Aún no tiene traspaso.",
|
|
499
|
+
"ag.rc.bad": "Toca rotar ({why}). Su traspaso no está completo.",
|
|
500
|
+
"ag.rc.fellBack":
|
|
501
|
+
"Toca rotar ({why}). No escribió el traspaso tras pedírselo 2 veces.",
|
|
502
|
+
"ag.rc.wanted":
|
|
503
|
+
"Toca rotar ({why}). Den lo relanza en cuanto escriba su traspaso.",
|
|
504
|
+
"ag.rc.write": "Escribir traspaso",
|
|
505
|
+
"ag.rc.writeName": "Pedir a {id} que escriba su traspaso",
|
|
506
|
+
"ag.rc.keep": "Mantener esta conversación",
|
|
507
|
+
"ag.rc.keepName": "Mantener la conversación de {id}",
|
|
508
|
+
"ag.rc.never": "No volver a preguntar para este agente",
|
|
509
|
+
"ag.rc.settings": "Ajustes",
|
|
510
|
+
"ag.rc.from":
|
|
511
|
+
"Rota tras {n} compactaciones ({src}); al tocar: {mode} ({src2})",
|
|
512
|
+
"ag.rc.fellBackBad":
|
|
513
|
+
"Toca rotar ({why}). Su traspaso sigue sin pasar la revisión tras pedírselo 2 veces.",
|
|
514
|
+
"ag.rc.from.one":
|
|
515
|
+
"Rota tras {n} compactación ({src}); al tocar: {mode} ({src2})",
|
|
516
|
+
"ag.rc.fromNever": "Nunca rota ({src}); al tocar: {mode} ({src2})",
|
|
517
|
+
"ag.rc.midTurn":
|
|
518
|
+
"{id} está a mitad de turno: pídelo de nuevo cuando termine",
|
|
519
|
+
"ag.rc.neverN": "nunca",
|
|
520
|
+
"ag.rc.mode.auto": "relanzar solo",
|
|
521
|
+
"ag.rc.mode.ask": "preguntar",
|
|
522
|
+
"ag.rc.mode.off": "nada",
|
|
523
|
+
"ag.rc.menu": "Ajustes de rotación",
|
|
524
|
+
"ag.rc.noState": "Aún no rota: cuenta desde su próximo inicio.",
|
|
525
|
+
"ag.rc.asked": "Se pidió a {id} su traspaso",
|
|
526
|
+
"ag.rc.askedRelaunch":
|
|
527
|
+
"Se pidió a {id} su traspaso. Den lo relanza en cuanto pase la revisión.",
|
|
528
|
+
"ag.rc.askedResumed":
|
|
529
|
+
"{id} reanudado para escribir su traspaso. Den lo relanza en cuanto pase la revisión.",
|
|
530
|
+
"ag.rc.kept": "Conversación de {id} mantenida: su cuenta vuelve a empezar",
|
|
531
|
+
"ag.rc.keptNever": "Conversación de {id} mantenida: ya no rotará",
|
|
532
|
+
"ag.rc.autoRelaunched":
|
|
533
|
+
"{id} relanzado desde su traspaso tras {why}. La conversación anterior {old} se conserva.",
|
|
493
534
|
"ag.remove": "Quitar",
|
|
494
535
|
"ag.removeName": "Quitar {id}",
|
|
495
536
|
"ag.removed": "{id} quitado",
|
|
@@ -705,6 +746,7 @@
|
|
|
705
746
|
"ver.pinned": "fijada {v}",
|
|
706
747
|
"ver.usedLatest": "{n} ahora sigue la versión más reciente",
|
|
707
748
|
"set.agents.personas": "Personas",
|
|
749
|
+
"set.agents.rotation": "Rotación",
|
|
708
750
|
// The Agents tab (AG-4a-term, agents-page.js).
|
|
709
751
|
"n.agents": "Agentes",
|
|
710
752
|
"kind.agent": "Agente",
|
|
@@ -473,9 +473,46 @@
|
|
|
473
473
|
"セッションは交代しました({why})が、handoff.md がありません。再開して書かせるか、新しく始めてください。",
|
|
474
474
|
"ag.rot.badHandoff":
|
|
475
475
|
"セッションは交代しました({why})が、handoff.md が不完全です。再開して直すか、新しく始めてください。",
|
|
476
|
-
"ag.relaunch": "
|
|
477
|
-
"ag.relaunchName": "{id}
|
|
476
|
+
"ag.relaunch": "今すぐ再起動",
|
|
477
|
+
"ag.relaunchName": "{id} を今すぐ再起動",
|
|
478
478
|
"ag.relaunched": "{id} を引き継ぎから再起動しました",
|
|
479
|
+
"ag.rc.autoAsk":
|
|
480
|
+
"交代の時期です({why})。待機中になったら Den が引き継ぎを書かせ、再起動します。",
|
|
481
|
+
"ag.rc.autoReady":
|
|
482
|
+
"交代の時期です({why})。引き継ぎは準備済み: 待機中になったら Den が再起動します。",
|
|
483
|
+
"ag.rc.ready": "交代の時期です({why})。引き継ぎは準備済みです。",
|
|
484
|
+
"ag.rc.missing": "交代の時期です({why})。引き継ぎがまだありません。",
|
|
485
|
+
"ag.rc.bad": "交代の時期です({why})。引き継ぎが不完全です。",
|
|
486
|
+
"ag.rc.fellBack":
|
|
487
|
+
"交代の時期です({why})。2 回頼んでも引き継ぎを書きませんでした。",
|
|
488
|
+
"ag.rc.wanted":
|
|
489
|
+
"交代の時期です({why})。引き継ぎが書かれたら Den が再起動します。",
|
|
490
|
+
"ag.rc.write": "引き継ぎを書かせる",
|
|
491
|
+
"ag.rc.writeName": "{id} に引き継ぎを書かせる",
|
|
492
|
+
"ag.rc.keep": "この会話を続ける",
|
|
493
|
+
"ag.rc.keepName": "{id} の会話を続ける",
|
|
494
|
+
"ag.rc.never": "このエージェントには今後たずねない",
|
|
495
|
+
"ag.rc.settings": "設定",
|
|
496
|
+
"ag.rc.from": "圧縮 {n} 回で交代({src})。時期が来たら: {mode}({src2})",
|
|
497
|
+
"ag.rc.fellBackBad":
|
|
498
|
+
"交代の時期です({why})。2 回頼んでも引き継ぎが確認を通りません。",
|
|
499
|
+
"ag.rc.fromNever": "交代しません({src})。時期が来たら: {mode}({src2})",
|
|
500
|
+
"ag.rc.midTurn": "{id} は応答中です: ターンが終わってから頼んでください",
|
|
501
|
+
"ag.rc.neverN": "しない",
|
|
502
|
+
"ag.rc.mode.auto": "自動で再起動",
|
|
503
|
+
"ag.rc.mode.ask": "たずねる",
|
|
504
|
+
"ag.rc.mode.off": "何もしない",
|
|
505
|
+
"ag.rc.menu": "交代の設定",
|
|
506
|
+
"ag.rc.noState": "まだ交代の記録がありません: 次の起動から数えます。",
|
|
507
|
+
"ag.rc.asked": "{id} に引き継ぎを頼みました",
|
|
508
|
+
"ag.rc.askedRelaunch":
|
|
509
|
+
"{id} に引き継ぎを頼みました。確認が通ったら Den が再起動します。",
|
|
510
|
+
"ag.rc.askedResumed":
|
|
511
|
+
"引き継ぎを書かせるため {id} を再開しました。確認が通ったら Den が再起動します。",
|
|
512
|
+
"ag.rc.kept": "{id} の会話を続けます: 回数はやり直しです",
|
|
513
|
+
"ag.rc.keptNever": "{id} の会話を続けます: 今後は交代しません",
|
|
514
|
+
"ag.rc.autoRelaunched":
|
|
515
|
+
"{why}のあと {id} を引き継ぎから再起動しました。前の会話 {old} は残っています。",
|
|
479
516
|
"ag.remove": "削除",
|
|
480
517
|
"ag.removeName": "{id} を削除",
|
|
481
518
|
"ag.removed": "{id} を削除しました",
|
|
@@ -691,6 +728,7 @@
|
|
|
691
728
|
"ver.pinned": "{v} に固定",
|
|
692
729
|
"ver.usedLatest": "{n} は最新バージョンに追従します",
|
|
693
730
|
"set.agents.personas": "ペルソナ",
|
|
731
|
+
"set.agents.rotation": "交代",
|
|
694
732
|
// The Agents tab (AG-4a-term, agents-page.js).
|
|
695
733
|
"n.agents": "エージェント",
|
|
696
734
|
"kind.agent": "エージェント",
|
|
@@ -889,6 +889,28 @@ export const DEN_SETTINGS = [
|
|
|
889
889
|
read: () => null,
|
|
890
890
|
},
|
|
891
891
|
|
|
892
|
+
{
|
|
893
|
+
id: "agents.rotation",
|
|
894
|
+
category: "agents",
|
|
895
|
+
label: "Rotation",
|
|
896
|
+
description:
|
|
897
|
+
"When an agent's conversation has compacted this many times (or its pull request merged), Den starts it again from its handoff: a fresh conversation, the same name, persona, workspace and folder. Auto asks the agent for its handoff when it is idle, then relaunches it by itself; Ask shows buttons on the agent; Off does nothing. Overrides win over this install's value: agent, persona in a workspace, persona, workspace, service. __DEN_CLI__ agents rotate",
|
|
898
|
+
keywords: [
|
|
899
|
+
"rotation",
|
|
900
|
+
"rotate",
|
|
901
|
+
"relaunch",
|
|
902
|
+
"handoff",
|
|
903
|
+
"compaction",
|
|
904
|
+
"compactions",
|
|
905
|
+
"context",
|
|
906
|
+
"restart",
|
|
907
|
+
"agent",
|
|
908
|
+
],
|
|
909
|
+
scope: "user",
|
|
910
|
+
control: { type: "custom", render: renderRotation },
|
|
911
|
+
read: () => null,
|
|
912
|
+
},
|
|
913
|
+
|
|
892
914
|
{
|
|
893
915
|
id: "agents.personas",
|
|
894
916
|
category: "agents",
|
|
@@ -1253,6 +1275,188 @@ function renderRuntimes(host, ctx) {
|
|
|
1253
1275
|
void load();
|
|
1254
1276
|
}
|
|
1255
1277
|
|
|
1278
|
+
const ROT_SCOPES = [
|
|
1279
|
+
["agent", "Agent", "agent id"],
|
|
1280
|
+
["persona-workspace", "Persona in a workspace", "persona@workspace"],
|
|
1281
|
+
["persona", "Persona", "persona id"],
|
|
1282
|
+
["workspace", "Workspace", "workspace"],
|
|
1283
|
+
["service", "Service", "service, as the workspace names it"],
|
|
1284
|
+
];
|
|
1285
|
+
const ROT_KEYS = [
|
|
1286
|
+
["after_compactions", "After compactions", "a number, or never"],
|
|
1287
|
+
["on_due", "When due", "auto, ask or off"],
|
|
1288
|
+
["on_pr_merged", "When its PR merges", "true or false"],
|
|
1289
|
+
];
|
|
1290
|
+
const rotVal = (v) => (v === undefined ? "" : String(v));
|
|
1291
|
+
|
|
1292
|
+
/** A value typed in the overrides form, as the key takes it. */
|
|
1293
|
+
export function rotationValue(key, raw) {
|
|
1294
|
+
const v = String(raw ?? "").trim();
|
|
1295
|
+
if (key === "after_compactions")
|
|
1296
|
+
return v === "never" || v === "0"
|
|
1297
|
+
? "never"
|
|
1298
|
+
: /^\d+$/.test(v)
|
|
1299
|
+
? Number(v)
|
|
1300
|
+
: v;
|
|
1301
|
+
if (key === "on_pr_merged")
|
|
1302
|
+
return v === "true" ? true : v === "false" ? false : v;
|
|
1303
|
+
return v;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** The Rotation row's HTML (b: GET /api/agents/rotation). */
|
|
1307
|
+
export function rotationHtml(b) {
|
|
1308
|
+
const inherited = (k) =>
|
|
1309
|
+
b.org?.[k] !== undefined
|
|
1310
|
+
? `org config: ${rotVal(b.org[k])}`
|
|
1311
|
+
: `default: ${rotVal(b.defaults?.[k])}`;
|
|
1312
|
+
const inst = b.install || {};
|
|
1313
|
+
const after = inst.after_compactions;
|
|
1314
|
+
const field = (k, control) =>
|
|
1315
|
+
`<div class="rot-f" data-rot-key="${k}"><label for="rot-${k}">${escR(ROT_KEYS.find((x) => x[0] === k)[1])}</label>${control}<span class="set-tag" data-rot-src>${escR(inst[k] !== undefined ? "this install" : inherited(k))}</span></div>`;
|
|
1316
|
+
const sel = (k, opts) =>
|
|
1317
|
+
`<select id="rot-${k}" data-focus-key="rot-${k}"><option value=""${inst[k] === undefined ? " selected" : ""}>inherit (${escR(inherited(k))})</option>` +
|
|
1318
|
+
opts
|
|
1319
|
+
.map(
|
|
1320
|
+
([v, l]) =>
|
|
1321
|
+
`<option value="${escR(v)}"${rotVal(inst[k]) === v ? " selected" : ""}>${escR(l)}</option>`,
|
|
1322
|
+
)
|
|
1323
|
+
.join("") +
|
|
1324
|
+
`</select>`;
|
|
1325
|
+
const rows = [];
|
|
1326
|
+
for (const [scope, byTarget] of Object.entries(b.overrides || {}))
|
|
1327
|
+
for (const [target, vals] of Object.entries(byTarget || {}))
|
|
1328
|
+
for (const [k, v] of Object.entries(vals || {}))
|
|
1329
|
+
rows.push(
|
|
1330
|
+
`<tr data-rot-row><td>${escR((ROT_SCOPES.find((x) => x[0] === scope) || [scope, scope])[1])}</td><td class="den-mono">${escR(target)}</td><td>${escR(k)}</td><td class="den-mono">${escR(rotVal(v))}</td>` +
|
|
1331
|
+
`<td><button type="button" class="btn sm" data-rot-clear data-scope="${escR(scope)}" data-target="${escR(target)}" data-key="${escR(k)}" aria-label="Clear ${escR(k)} for ${escR(scope)} ${escR(target)}">Clear</button></td></tr>`,
|
|
1332
|
+
);
|
|
1333
|
+
return (
|
|
1334
|
+
`<div class="rot" data-rotation-settings>` +
|
|
1335
|
+
`<div class="rot-inst">` +
|
|
1336
|
+
field(
|
|
1337
|
+
"after_compactions",
|
|
1338
|
+
`<input type="text" id="rot-after_compactions" data-focus-key="rot-after_compactions" inputmode="numeric" spellcheck="false" autocomplete="off" placeholder="${escR(inherited("after_compactions"))}" value="${escR(after === undefined ? "" : rotVal(after))}"> <button type="button" class="btn sm" data-rot-save-after>Save</button>`,
|
|
1339
|
+
) +
|
|
1340
|
+
field(
|
|
1341
|
+
"on_due",
|
|
1342
|
+
sel("on_due", [
|
|
1343
|
+
["auto", "Auto"],
|
|
1344
|
+
["ask", "Ask"],
|
|
1345
|
+
["off", "Off"],
|
|
1346
|
+
]),
|
|
1347
|
+
) +
|
|
1348
|
+
field(
|
|
1349
|
+
"on_pr_merged",
|
|
1350
|
+
sel("on_pr_merged", [
|
|
1351
|
+
["true", "Relaunch"],
|
|
1352
|
+
["false", "Keep going"],
|
|
1353
|
+
]),
|
|
1354
|
+
) +
|
|
1355
|
+
`</div>` +
|
|
1356
|
+
`<div class="tablewrap"><table class="svc rot-ov"><caption>Overrides (the most specific wins)</caption><thead><tr><th scope="col">Scope</th><th scope="col">For</th><th scope="col">Setting</th><th scope="col">Value</th><th scope="col" aria-label="Actions"></th></tr></thead><tbody>` +
|
|
1357
|
+
(rows.length
|
|
1358
|
+
? rows.join("")
|
|
1359
|
+
: `<tr><td colspan="5" class="set-note">No overrides: every agent uses this install's values.</td></tr>`) +
|
|
1360
|
+
`</tbody></table></div>` +
|
|
1361
|
+
`<form class="rot-add" data-rot-add>` +
|
|
1362
|
+
`<label for="rot-a-scope">Scope</label> <select id="rot-a-scope">${ROT_SCOPES.map(([v, l]) => `<option value="${v}">${escR(l)}</option>`).join("")}</select> ` +
|
|
1363
|
+
`<label for="rot-a-target">For</label> <input type="text" id="rot-a-target" class="den-mono" spellcheck="false" autocomplete="off" placeholder="agent id" required> ` +
|
|
1364
|
+
`<label for="rot-a-key">Setting</label> <select id="rot-a-key">${ROT_KEYS.map(([v, l]) => `<option value="${v}">${escR(l)}</option>`).join("")}</select> ` +
|
|
1365
|
+
`<label for="rot-a-value">Value</label> <input type="text" id="rot-a-value" class="den-mono" spellcheck="false" autocomplete="off" placeholder="a number, or never" required> ` +
|
|
1366
|
+
`<button type="submit" class="btn sm primary">Add override</button></form>` +
|
|
1367
|
+
`<div class="rt-err" role="alert" hidden></div>` +
|
|
1368
|
+
`</div>`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/** The Rotation row: this install's values, the overrides, + Add override. */
|
|
1373
|
+
function renderRotation(host, ctx) {
|
|
1374
|
+
const err = () => host.querySelector(".rt-err");
|
|
1375
|
+
const fail = (x) => {
|
|
1376
|
+
const e = err();
|
|
1377
|
+
if (e) {
|
|
1378
|
+
e.textContent = x?.message || "Not saved";
|
|
1379
|
+
e.hidden = false;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
const load = () => {
|
|
1383
|
+
if (ctx.shell?.configInvalid?.() === true) {
|
|
1384
|
+
host.innerHTML = `<div class="set-note">Fix the config first: the org's rotation is read from it.</div>`;
|
|
1385
|
+
return Promise.resolve();
|
|
1386
|
+
}
|
|
1387
|
+
return ctx.shell
|
|
1388
|
+
.api("GET", "/api/agents/rotation")
|
|
1389
|
+
.then((b) => {
|
|
1390
|
+
host.innerHTML = rotationHtml(b || {});
|
|
1391
|
+
})
|
|
1392
|
+
.catch((x) => {
|
|
1393
|
+
host.innerHTML = `<div class="set-note">${escR(x?.message || "The rotation settings did not load.")}</div>`;
|
|
1394
|
+
});
|
|
1395
|
+
};
|
|
1396
|
+
const put = async (body) => {
|
|
1397
|
+
try {
|
|
1398
|
+
await ctx.shell.api("PUT", "/api/agents/rotation", body);
|
|
1399
|
+
await load();
|
|
1400
|
+
} catch (x) {
|
|
1401
|
+
fail(x);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
host.addEventListener("change", (e) => {
|
|
1405
|
+
const k = e.target.closest("[data-rot-key]")?.dataset.rotKey;
|
|
1406
|
+
if (e.target.id === "rot-a-scope") {
|
|
1407
|
+
const s = ROT_SCOPES.find((x) => x[0] === e.target.value);
|
|
1408
|
+
const t = host.querySelector("#rot-a-target");
|
|
1409
|
+
if (s && t) t.placeholder = s[2];
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (e.target.id === "rot-a-key") {
|
|
1413
|
+
const s = ROT_KEYS.find((x) => x[0] === e.target.value);
|
|
1414
|
+
const v = host.querySelector("#rot-a-value");
|
|
1415
|
+
if (s && v) v.placeholder = s[2];
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (!k || k === "after_compactions" || e.target.tagName !== "SELECT")
|
|
1419
|
+
return;
|
|
1420
|
+
const v = e.target.value;
|
|
1421
|
+
void put({
|
|
1422
|
+
scope: "install",
|
|
1423
|
+
key: k,
|
|
1424
|
+
value: v === "" ? null : rotationValue(k, v),
|
|
1425
|
+
});
|
|
1426
|
+
});
|
|
1427
|
+
host.addEventListener("click", (e) => {
|
|
1428
|
+
const b = e.target.closest("button");
|
|
1429
|
+
if (!b) return;
|
|
1430
|
+
if (b.hasAttribute("data-rot-save-after")) {
|
|
1431
|
+
const v = host.querySelector("#rot-after_compactions")?.value.trim();
|
|
1432
|
+
void put({
|
|
1433
|
+
scope: "install",
|
|
1434
|
+
key: "after_compactions",
|
|
1435
|
+
value: v ? rotationValue("after_compactions", v) : null,
|
|
1436
|
+
});
|
|
1437
|
+
} else if (b.hasAttribute("data-rot-clear"))
|
|
1438
|
+
void put({
|
|
1439
|
+
scope: b.dataset.scope,
|
|
1440
|
+
target: b.dataset.target,
|
|
1441
|
+
key: b.dataset.key,
|
|
1442
|
+
value: null,
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
host.addEventListener("submit", (e) => {
|
|
1446
|
+
const f = e.target.closest("[data-rot-add]");
|
|
1447
|
+
if (!f) return;
|
|
1448
|
+
e.preventDefault();
|
|
1449
|
+
const key = f.querySelector("#rot-a-key").value;
|
|
1450
|
+
void put({
|
|
1451
|
+
scope: f.querySelector("#rot-a-scope").value,
|
|
1452
|
+
target: f.querySelector("#rot-a-target").value.trim(),
|
|
1453
|
+
key,
|
|
1454
|
+
value: rotationValue(key, f.querySelector("#rot-a-value").value),
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
void load();
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1256
1460
|
/** Register Den's categories and rows with the shell's SETTINGS registry. */
|
|
1257
1461
|
export function registerDenSettings(shell) {
|
|
1258
1462
|
for (const c of DEN_SETTING_CATEGORIES) shell.registerSettingsCategory(c);
|
|
@@ -384,10 +384,9 @@
|
|
|
384
384
|
}
|
|
385
385
|
.rt {
|
|
386
386
|
display: grid;
|
|
387
|
-
grid-template-columns:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
) auto;
|
|
387
|
+
grid-template-columns:
|
|
388
|
+
minmax(0, 1.2fr) minmax(0, 0.8fr) minmax(0, 2fr)
|
|
389
|
+
auto;
|
|
391
390
|
align-items: center;
|
|
392
391
|
gap: 4px 12px;
|
|
393
392
|
padding: 8px 10px;
|
|
@@ -427,6 +426,46 @@
|
|
|
427
426
|
.rt-err[hidden] {
|
|
428
427
|
display: none;
|
|
429
428
|
}
|
|
429
|
+
/* D28: Settings › Agents › Rotation. */
|
|
430
|
+
.rot-inst {
|
|
431
|
+
display: grid;
|
|
432
|
+
gap: 8px;
|
|
433
|
+
margin-bottom: 12px;
|
|
434
|
+
}
|
|
435
|
+
.rot-f,
|
|
436
|
+
.rot-add {
|
|
437
|
+
display: flex;
|
|
438
|
+
flex-wrap: wrap;
|
|
439
|
+
align-items: center;
|
|
440
|
+
gap: 6px;
|
|
441
|
+
min-width: 0;
|
|
442
|
+
}
|
|
443
|
+
.rot-f label {
|
|
444
|
+
flex: 0 0 150px;
|
|
445
|
+
}
|
|
446
|
+
.rot-f input,
|
|
447
|
+
.rot-f select,
|
|
448
|
+
.rot-add input,
|
|
449
|
+
.rot-add select {
|
|
450
|
+
flex: 1 1 140px;
|
|
451
|
+
min-width: 0;
|
|
452
|
+
max-width: 100%;
|
|
453
|
+
}
|
|
454
|
+
.rot-add {
|
|
455
|
+
margin-top: 10px;
|
|
456
|
+
}
|
|
457
|
+
.rot-ov caption {
|
|
458
|
+
text-align: left;
|
|
459
|
+
color: var(--muted);
|
|
460
|
+
font-size: 12.5px;
|
|
461
|
+
padding-bottom: 4px;
|
|
462
|
+
}
|
|
463
|
+
.rot-ov th,
|
|
464
|
+
.rot-ov td {
|
|
465
|
+
padding: 4px 10px 4px 0;
|
|
466
|
+
text-align: left;
|
|
467
|
+
overflow-wrap: anywhere;
|
|
468
|
+
}
|
|
430
469
|
.rt-fl {
|
|
431
470
|
flex-basis: 100%;
|
|
432
471
|
color: var(--muted);
|
|
@@ -611,9 +611,48 @@
|
|
|
611
611
|
"Its session rotated ({why}), but it wrote no handoff.md. Resume it to write one, or start fresh.",
|
|
612
612
|
"ag.rot.badHandoff":
|
|
613
613
|
"Its session rotated ({why}), but its handoff.md is not complete. Resume it to fix it, or start fresh.",
|
|
614
|
-
"ag.relaunch": "Relaunch
|
|
615
|
-
"ag.relaunchName": "Relaunch {id}
|
|
614
|
+
"ag.relaunch": "Relaunch now",
|
|
615
|
+
"ag.relaunchName": "Relaunch {id} now",
|
|
616
616
|
"ag.relaunched": "Relaunched {id} from its handoff",
|
|
617
|
+
"ag.rc.autoAsk":
|
|
618
|
+
"Due ({why}). Den asks it for its handoff when it is idle, then relaunches it.",
|
|
619
|
+
"ag.rc.autoReady":
|
|
620
|
+
"Due ({why}). Its handoff is ready: Den relaunches it when it is idle.",
|
|
621
|
+
"ag.rc.ready": "Due ({why}). Its handoff is ready.",
|
|
622
|
+
"ag.rc.missing": "Due ({why}). It has no handoff yet.",
|
|
623
|
+
"ag.rc.bad": "Due ({why}). Its handoff is not complete.",
|
|
624
|
+
"ag.rc.fellBack": "Due ({why}). It wrote no handoff after 2 asks.",
|
|
625
|
+
"ag.rc.wanted":
|
|
626
|
+
"Due ({why}). Den relaunches it once its handoff is written.",
|
|
627
|
+
"ag.rc.write": "Write handoff",
|
|
628
|
+
"ag.rc.writeName": "Ask {id} to write its handoff",
|
|
629
|
+
"ag.rc.keep": "Keep this conversation",
|
|
630
|
+
"ag.rc.keepName": "Keep {id}'s conversation",
|
|
631
|
+
"ag.rc.never": "Don't ask again for this agent",
|
|
632
|
+
"ag.rc.settings": "Settings",
|
|
633
|
+
"ag.rc.from":
|
|
634
|
+
"Rotates after {n} compactions ({src}); when due: {mode} ({src2})",
|
|
635
|
+
"ag.rc.fellBackBad":
|
|
636
|
+
"Due ({why}). Its handoff still fails the check after 2 asks.",
|
|
637
|
+
"ag.rc.from.one":
|
|
638
|
+
"Rotates after {n} compaction ({src}); when due: {mode} ({src2})",
|
|
639
|
+
"ag.rc.fromNever": "Never rotates ({src}); when due: {mode} ({src2})",
|
|
640
|
+
"ag.rc.midTurn": "{id} is mid-turn: ask again when its turn ends",
|
|
641
|
+
"ag.rc.neverN": "never",
|
|
642
|
+
"ag.rc.mode.auto": "relaunch by itself",
|
|
643
|
+
"ag.rc.mode.ask": "ask",
|
|
644
|
+
"ag.rc.mode.off": "nothing",
|
|
645
|
+
"ag.rc.menu": "Rotation settings",
|
|
646
|
+
"ag.rc.noState": "No rotation yet: it counts from its next start.",
|
|
647
|
+
"ag.rc.asked": "Asked {id} for its handoff",
|
|
648
|
+
"ag.rc.askedRelaunch":
|
|
649
|
+
"Asked {id} for its handoff. Den relaunches it once it passes.",
|
|
650
|
+
"ag.rc.askedResumed":
|
|
651
|
+
"Resumed {id} to write its handoff. Den relaunches it once it passes.",
|
|
652
|
+
"ag.rc.kept": "Kept {id}'s conversation: its count starts again",
|
|
653
|
+
"ag.rc.keptNever": "Kept {id}'s conversation: it won't rotate again",
|
|
654
|
+
"ag.rc.autoRelaunched":
|
|
655
|
+
"Relaunched {id} from its handoff after {why}. The old conversation {old} is kept.",
|
|
617
656
|
"ag.interrupted": "{n} agents were interrupted when {name} restarted",
|
|
618
657
|
"ag.interrupted.one": "{n} agent was interrupted when {name} restarted",
|
|
619
658
|
"ag.resumeAll": "Resume all",
|
|
@@ -815,6 +854,7 @@
|
|
|
815
854
|
"ver.pinned": "pinned {v}",
|
|
816
855
|
"ver.usedLatest": "{n} now tracks the latest version",
|
|
817
856
|
"set.agents.personas": "Personas",
|
|
857
|
+
"set.agents.rotation": "Rotation",
|
|
818
858
|
// The Agents tab (AG-4a-term, agents-page.js).
|
|
819
859
|
"n.agents": "Agents",
|
|
820
860
|
"kind.agent": "Agent",
|
|
@@ -374,6 +374,25 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
374
374
|
"forgets every stopped agent of the workspaces named (AG-14.1b)",
|
|
375
375
|
),
|
|
376
376
|
M(["POST"], "/api/agents/restore", "Undo: brings removed agents back"),
|
|
377
|
+
M(
|
|
378
|
+
["POST"],
|
|
379
|
+
"/api/workspaces/:ws/agents/:id/keep",
|
|
380
|
+
"D28 Keep this conversation: resets its rotation count (and, asked, sets its override to never); a person's only",
|
|
381
|
+
),
|
|
382
|
+
S(
|
|
383
|
+
["POST"],
|
|
384
|
+
"/api/workspaces/:ws/agents/:id/handoff",
|
|
385
|
+
"D28 Write handoff: types the handoff prompt into the agent's running session",
|
|
386
|
+
),
|
|
387
|
+
R(
|
|
388
|
+
"/api/agents/rotation",
|
|
389
|
+
"D28: rotation settings, the overrides and an agent's effective values",
|
|
390
|
+
),
|
|
391
|
+
M(
|
|
392
|
+
["PUT"],
|
|
393
|
+
"/api/agents/rotation",
|
|
394
|
+
"D28: sets or clears one rotation setting at one scope; a person's only",
|
|
395
|
+
),
|
|
377
396
|
S(
|
|
378
397
|
["POST"],
|
|
379
398
|
"/api/workspaces/:ws/agents/:id/fresh",
|
|
@@ -504,6 +523,8 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
|
504
523
|
"/api/workspaces/:ws/agents/:id/name",
|
|
505
524
|
"/api/workspaces/:ws/agents/:id/paths",
|
|
506
525
|
"/api/workspaces/:ws/agents/:id/persona",
|
|
526
|
+
"/api/workspaces/:ws/agents/:id/keep",
|
|
527
|
+
"/api/workspaces/:ws/agents/:id/handoff",
|
|
507
528
|
],
|
|
508
529
|
"agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
|
|
509
530
|
"agent-routes.ts:PERSONA_PATH": ["/api/personas/:id"],
|
|
@@ -50,7 +50,11 @@ import { revokedAgentIds } from "../../shared/actor-token.js";
|
|
|
50
50
|
import { listRecords } from "../../shared/agents/records.js";
|
|
51
51
|
import { softNewlineSeq } from "../../shared/agents/catalog.js";
|
|
52
52
|
import { getRuntime } from "../../shared/agents/runtimes.js";
|
|
53
|
-
import type {
|
|
53
|
+
import type {
|
|
54
|
+
RotationEntry,
|
|
55
|
+
RotationInfo,
|
|
56
|
+
RotationPolicy,
|
|
57
|
+
} from "../../shared/agents/rotation.js";
|
|
54
58
|
import {
|
|
55
59
|
connectHost,
|
|
56
60
|
noHost,
|
|
@@ -61,7 +65,7 @@ import { sessionPaths } from "../../shared/sessions/paths.js";
|
|
|
61
65
|
import { loadPty, type PtyLoad } from "../../shared/sessions/pty.js";
|
|
62
66
|
import type { SessionInfo } from "../../shared/sessions/types.js";
|
|
63
67
|
import { agentRows, newerOf, noteEnded } from "../cli/agent-ops.js";
|
|
64
|
-
import type { Override } from "../../shared/agents/records.js";
|
|
68
|
+
import type { AgentRecord, Override } from "../../shared/agents/records.js";
|
|
65
69
|
import { state as sessionState } from "../cli/sessions-command.js";
|
|
66
70
|
import { recordAction } from "./audit.js";
|
|
67
71
|
import { sendJson } from "./http-helpers.js";
|
|
@@ -85,6 +89,8 @@ export interface SessionRouteDeps {
|
|
|
85
89
|
pty?: () => PtyLoad;
|
|
86
90
|
/** The agent ids with an approval waiting for the person (AG-6a, ON-3). */
|
|
87
91
|
waitingAgents?: () => ReadonlySet<string>;
|
|
92
|
+
/** D28: each agent's effective rotation settings (one resolver per list). */
|
|
93
|
+
rotationPolicy?: () => (rec: AgentRecord) => RotationPolicy | undefined;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
interface Resolved {
|
|
@@ -95,6 +101,7 @@ interface Resolved {
|
|
|
95
101
|
workspaceExists: (ws: string) => boolean;
|
|
96
102
|
pty: () => PtyLoad;
|
|
97
103
|
waitingAgents: () => ReadonlySet<string>;
|
|
104
|
+
rotationPolicy?: SessionRouteDeps["rotationPolicy"];
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
/** A workspace name as `?ws=` may give it. */
|
|
@@ -125,6 +132,7 @@ function resolve(d: SessionRouteDeps = {}): Resolved {
|
|
|
125
132
|
)),
|
|
126
133
|
pty: d.pty ?? (() => loadPty()),
|
|
127
134
|
waitingAgents: d.waitingAgents ?? (() => new Set<string>()),
|
|
135
|
+
...(d.rotationPolicy ? { rotationPolicy: d.rotationPolicy } : {}),
|
|
128
136
|
};
|
|
129
137
|
}
|
|
130
138
|
|
|
@@ -199,13 +207,16 @@ export interface ListedSession {
|
|
|
199
207
|
* Relaunch from handoff is its action, not Resume.
|
|
200
208
|
*/
|
|
201
209
|
rotation?: RotationInfo;
|
|
210
|
+
/** D28: the last relaunch from a handoff (the page toasts one Den did by itself). */
|
|
211
|
+
lastRotation?: RotationEntry;
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
/** The list, from the host's sessions and the agent records. */
|
|
205
215
|
export function listedSessions(
|
|
206
216
|
sessions: SessionInfo[],
|
|
207
217
|
d: Pick<Resolved, "stateDir" | "dist" | "activeWorkspaces"> &
|
|
208
|
-
Partial<Pick<Resolved, "waitingAgents"
|
|
218
|
+
Partial<Pick<Resolved, "waitingAgents">> &
|
|
219
|
+
Pick<SessionRouteDeps, "rotationPolicy">,
|
|
209
220
|
/** A workspace asked for by name (`?ws=`), listed even when inactive. */
|
|
210
221
|
also?: string,
|
|
211
222
|
): ListedSession[] {
|
|
@@ -226,7 +237,12 @@ export function listedSessions(
|
|
|
226
237
|
const out: ListedSession[] = [];
|
|
227
238
|
const agentSessions = new Set<string>();
|
|
228
239
|
noteEnded(d.stateDir(), mine, records);
|
|
229
|
-
for (const r of agentRows(
|
|
240
|
+
for (const r of agentRows(
|
|
241
|
+
mine,
|
|
242
|
+
records,
|
|
243
|
+
d.stateDir(),
|
|
244
|
+
d.rotationPolicy?.(),
|
|
245
|
+
)) {
|
|
230
246
|
const rec = records.find((x) => x.id === r.agent)!;
|
|
231
247
|
const s = r.session;
|
|
232
248
|
if (s) agentSessions.add(s.ref.id);
|
|
@@ -262,7 +278,9 @@ export function listedSessions(
|
|
|
262
278
|
...(r.stoppedAt ? { stoppedAt: r.stoppedAt } : {}),
|
|
263
279
|
...(r.endedAt ? { endedAt: r.endedAt } : {}),
|
|
264
280
|
...(r.interruptedAt ? { interruptedAt: r.interruptedAt } : {}),
|
|
265
|
-
|
|
281
|
+
// D28: always (its effective settings show), due or not.
|
|
282
|
+
...(r.rotation ? { rotation: r.rotation } : {}),
|
|
283
|
+
...(r.lastRotation ? { lastRotation: r.lastRotation } : {}),
|
|
266
284
|
...(revoked.has(rec.id) && s && !s.exited ? { tokenRevoked: true } : {}),
|
|
267
285
|
...(waiting.has(rec.id) && s && !s.exited ? { waiting: true } : {}),
|
|
268
286
|
});
|