@monster-orange/den 0.4.8 → 0.4.10
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 +105 -0
- package/README.md +7 -7
- package/docs/distributions.md +128 -1
- package/package.json +1 -1
- package/src/shared/agents/den-claude.ts +38 -3
- package/src/shared/agents/guardrails/hooks.json +23 -0
- package/src/shared/agents/guardrails/instructions.md +23 -0
- package/src/shared/agents/guardrails/rotation-hook.ts +127 -0
- package/src/shared/agents/guardrails/stop-hook.ts +48 -2
- package/src/shared/agents/guardrails.ts +38 -7
- package/src/shared/agents/handoff.ts +118 -0
- package/src/shared/agents/records.ts +13 -1
- package/src/shared/agents/rotation.ts +371 -0
- package/src/shared/agents/runtime.ts +15 -0
- package/src/shared/den-config.ts +42 -1
- package/src/shared/git/gh-pr-state.ts +44 -0
- package/src/shared/usage/report.ts +117 -4
- package/src/shared/usage/transcript.ts +62 -5
- package/src/ui/cli/agent-ops.ts +196 -7
- package/src/ui/cli/agents-command.ts +75 -11
- package/src/ui/cli/usage-command.ts +95 -11
- package/src/ui/server/agent-routes.ts +19 -2
- package/src/ui/server/public/agents-page.js +78 -13
- package/src/ui/server/public/i18n/es.js +12 -0
- package/src/ui/server/public/i18n/ja.js +11 -0
- package/src/ui/server/public/shell.js +13 -0
- package/src/ui/server/route-classes.ts +6 -0
- package/src/ui/server/session-routes.ts +8 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `<cli> usage` (den#6): how agents spend tokens and time, read from the
|
|
3
3
|
* runtimes' own transcripts on this machine (usage sources, plugin kind
|
|
4
|
-
* `usage`).
|
|
5
|
-
* local files.
|
|
4
|
+
* `usage`). None of it is sent anywhere: it reads local files and writes
|
|
5
|
+
* local files. The one question it asks outside: when an agent ran
|
|
6
|
+
* `gh pr merge`, `gh pr view <ref> --json number,state` confirms the merge
|
|
7
|
+
* (gh-pr-state.ts) before it counts.
|
|
6
8
|
*
|
|
7
9
|
* agents [-w <ws>] [--json] each agent's tokens and outcome (PR opened
|
|
8
10
|
* or merged, tests green, wall-clock), joined
|
|
@@ -15,10 +17,15 @@
|
|
|
15
17
|
import * as fs from "fs";
|
|
16
18
|
import * as path from "path";
|
|
17
19
|
import {
|
|
20
|
+
agentFilesDir,
|
|
18
21
|
listRecords,
|
|
19
22
|
readRecord,
|
|
20
23
|
writeRecord,
|
|
21
24
|
} from "../../shared/agents/records.js";
|
|
25
|
+
import { clearDue, markDue } from "../../shared/agents/rotation.js";
|
|
26
|
+
import { ghPrState } from "../../shared/git/gh-pr-state.js";
|
|
27
|
+
import type { PrStateCheck } from "../../shared/usage/report.js";
|
|
28
|
+
import { readAgentsConfig } from "../../shared/den-config.js";
|
|
22
29
|
import { getStateDir } from "../../shared/paths.js";
|
|
23
30
|
import { product } from "../../shared/product.js";
|
|
24
31
|
import {
|
|
@@ -48,11 +55,14 @@ export const USAGE_HELP = {
|
|
|
48
55
|
"",
|
|
49
56
|
"{cli} usage agents [-w <ws>] [--json]",
|
|
50
57
|
" each agent's tokens and outcome (pull request opened or merged, tests",
|
|
51
|
-
" green, wall-clock),
|
|
52
|
-
" record; then the share of agents
|
|
58
|
+
" green, wall-clock), compactions and rotations, joined on its",
|
|
59
|
+
" conversation id, and saved in its record; then the share of agents",
|
|
60
|
+
" with both. A merged pull request makes the agent's rotation due",
|
|
53
61
|
"{cli} usage report [--days <n>] [--json]",
|
|
54
62
|
" the last n days (default 7): tokens, context re-reads, polling, denials,",
|
|
55
|
-
" tool errors, corrections, subagents on Opus
|
|
63
|
+
" tool errors, corrections, subagents on Opus, the longest session's context",
|
|
64
|
+
" per call, the share of re-reads from never-ending sessions (more",
|
|
65
|
+
" compactions than agents.rotate.after_compactions), rotations; written to",
|
|
56
66
|
" <state dir>/usage/<date>.json and .md, compared with the last report",
|
|
57
67
|
],
|
|
58
68
|
};
|
|
@@ -64,6 +74,37 @@ export interface UsageDeps {
|
|
|
64
74
|
out?: (line: string) => void;
|
|
65
75
|
err?: (line: string) => void;
|
|
66
76
|
cli?: string;
|
|
77
|
+
/** agents.rotate (default: the org config's). */
|
|
78
|
+
rotateAfter?: () => { afterCompactions: number } | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* A pull request's state, to confirm a `gh pr merge` the agent ran
|
|
81
|
+
* (default: `gh pr view <ref> --json number,state` in the agent's folder).
|
|
82
|
+
*/
|
|
83
|
+
prState?: PrStateCheck;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The check, counting how often gh ran (`asked`) and how often it could not
|
|
88
|
+
* run because gh is not installed (`missing`); the summary says both.
|
|
89
|
+
*/
|
|
90
|
+
function counted(
|
|
91
|
+
f: NonNullable<UsageDeps["prState"]>,
|
|
92
|
+
): NonNullable<UsageDeps["prState"]> & { asked: number; missing: number } {
|
|
93
|
+
const g = Object.assign(
|
|
94
|
+
async (ref: string, o: { repo?: string; cwd: string }) => {
|
|
95
|
+
try {
|
|
96
|
+
const st = await f(ref, o);
|
|
97
|
+
g.asked++;
|
|
98
|
+
return st;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if ((e as NodeJS.ErrnoException)?.code === "ENOENT") g.missing++;
|
|
101
|
+
else g.asked++;
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{ asked: 0, missing: 0 },
|
|
106
|
+
);
|
|
107
|
+
return g;
|
|
67
108
|
}
|
|
68
109
|
|
|
69
110
|
function resolve(d: UsageDeps) {
|
|
@@ -74,6 +115,8 @@ function resolve(d: UsageDeps) {
|
|
|
74
115
|
out: d.out ?? ((l: string) => console.log(l)),
|
|
75
116
|
err: d.err ?? ((l: string) => console.error(l)),
|
|
76
117
|
cli: d.cli ?? product().cli,
|
|
118
|
+
rotateLimit: () => rotateLimit(d),
|
|
119
|
+
prState: counted(d.prState ?? ghPrState(d.env ?? process.env)),
|
|
77
120
|
};
|
|
78
121
|
}
|
|
79
122
|
|
|
@@ -92,7 +135,11 @@ const mins = (ms?: number) =>
|
|
|
92
135
|
? `${Math.round(ms / 60_000)}m`
|
|
93
136
|
: `${(ms / 3_600_000).toFixed(1)}h`;
|
|
94
137
|
|
|
95
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* Save each attributed row's usage and outcome in its agent record. An
|
|
140
|
+
* agent whose pull request merged has its rotation marked due (den#4): its
|
|
141
|
+
* Stop hook then asks it for its handoff.
|
|
142
|
+
*/
|
|
96
143
|
function save(stateDir: string, rows: readonly AgentUsageRow[]): void {
|
|
97
144
|
for (const r of rows) {
|
|
98
145
|
if (!r.usage) continue;
|
|
@@ -103,6 +150,23 @@ function save(stateDir: string, rows: readonly AgentUsageRow[]): void {
|
|
|
103
150
|
usage: r.usage,
|
|
104
151
|
...(r.outcome ? { outcome: r.outcome } : {}),
|
|
105
152
|
});
|
|
153
|
+
if (r.outcome?.prMerged)
|
|
154
|
+
markDue(agentFilesDir(stateDir, rec.id), "pr-merged", rec.resumeToken);
|
|
155
|
+
// A mark gh no longer confirms is dropped, and its rotation with it.
|
|
156
|
+
else if (rec.outcome?.prMerged)
|
|
157
|
+
clearDue(agentFilesDir(stateDir, rec.id), "pr-merged");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** agents.rotate.after_compactions; the default when the config can't be read. */
|
|
162
|
+
function rotateLimit(d: UsageDeps): number {
|
|
163
|
+
try {
|
|
164
|
+
return (
|
|
165
|
+
(d.rotateAfter ?? (() => readAgentsConfig().rotate))()
|
|
166
|
+
?.afterCompactions ?? 3
|
|
167
|
+
);
|
|
168
|
+
} catch {
|
|
169
|
+
return 3;
|
|
106
170
|
}
|
|
107
171
|
}
|
|
108
172
|
|
|
@@ -117,7 +181,10 @@ async function agents(
|
|
|
117
181
|
const recs = listRecords(d.stateDir()).filter(
|
|
118
182
|
(r) => !ws || r.workspace === ws,
|
|
119
183
|
);
|
|
120
|
-
const rows = await agentUsage(recs, {
|
|
184
|
+
const rows = await agentUsage(recs, {
|
|
185
|
+
env: d.env,
|
|
186
|
+
prState: d.prState,
|
|
187
|
+
});
|
|
121
188
|
save(d.stateDir(), rows);
|
|
122
189
|
const pct = attributedPct(rows);
|
|
123
190
|
if (p.flags["--json"]) {
|
|
@@ -138,6 +205,8 @@ async function agents(
|
|
|
138
205
|
"WALL",
|
|
139
206
|
"TESTS",
|
|
140
207
|
"PR",
|
|
208
|
+
"COMPACTIONS",
|
|
209
|
+
"ROTATIONS",
|
|
141
210
|
],
|
|
142
211
|
...rows.map((r) => [
|
|
143
212
|
r.agent,
|
|
@@ -153,15 +222,27 @@ async function agents(
|
|
|
153
222
|
: "red",
|
|
154
223
|
r.outcome?.prOpened
|
|
155
224
|
? `${r.outcome.prOpened}${r.outcome.prMerged ? " (merged)" : ""}`
|
|
156
|
-
:
|
|
225
|
+
: r.outcome?.prMergeConfirmed
|
|
226
|
+
? `${r.outcome.prMergeConfirmed} (merged)`
|
|
227
|
+
: "-",
|
|
228
|
+
r.compactions === undefined ? "-" : String(r.compactions),
|
|
229
|
+
String(r.rotations),
|
|
157
230
|
]),
|
|
158
231
|
]))
|
|
159
232
|
d.out(l);
|
|
160
233
|
for (const r of rows) if (r.why) d.out(` ${r.agent}: ${r.why}`);
|
|
161
234
|
d.out("");
|
|
162
235
|
d.out(
|
|
163
|
-
`${pct}% of agents have their tokens and outcome (${rows.filter((r) => r.attributed).length} of ${rows.length}). Read locally;
|
|
236
|
+
`${pct}% of agents have their tokens and outcome (${rows.filter((r) => r.attributed).length} of ${rows.length}). Read locally; none of this was sent.`,
|
|
164
237
|
);
|
|
238
|
+
if (d.prState.asked)
|
|
239
|
+
d.out(
|
|
240
|
+
`To confirm a merge, it asked GitHub for the state of ${d.prState.asked} pull request${d.prState.asked === 1 ? "" : "s"} (gh pr view).`,
|
|
241
|
+
);
|
|
242
|
+
if (d.prState.missing)
|
|
243
|
+
d.out(
|
|
244
|
+
`Couldn't check ${d.prState.missing} merge${d.prState.missing === 1 ? "" : "s"}: gh is not installed.`,
|
|
245
|
+
);
|
|
165
246
|
return 0;
|
|
166
247
|
}
|
|
167
248
|
|
|
@@ -209,9 +290,12 @@ async function report(
|
|
|
209
290
|
for (const src of usageSources())
|
|
210
291
|
for (const f of src.files(since, d.env))
|
|
211
292
|
stats.push(await src.mine(f, { since }));
|
|
212
|
-
const rows = await agentUsage(listRecords(d.stateDir()), {
|
|
293
|
+
const rows = await agentUsage(listRecords(d.stateDir()), {
|
|
294
|
+
env: d.env,
|
|
295
|
+
prState: d.prState,
|
|
296
|
+
});
|
|
213
297
|
save(d.stateDir(), rows);
|
|
214
|
-
const m = weeklyMetrics(stats, rows);
|
|
298
|
+
const m = weeklyMetrics(stats, rows, d.rotateLimit());
|
|
215
299
|
const date = now.toISOString().slice(0, 10);
|
|
216
300
|
const dir = path.join(d.stateDir(), "usage");
|
|
217
301
|
const prev = previous(dir, date);
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* POST /api/workspaces/<ws>/agents/<id>/resume the same conversation (AG-14)
|
|
19
19
|
* POST /api/workspaces/<ws>/agents/<id>/fresh a new conversation, same
|
|
20
20
|
* persona and workspace
|
|
21
|
+
* POST /api/workspaces/<ws>/agents/<id>/relaunch a rotated agent (den#4):
|
|
22
|
+
* a new conversation whose
|
|
23
|
+
* first prompt is its
|
|
24
|
+
* handoff.md; a resume of
|
|
25
|
+
* one is refused (409 rotated)
|
|
21
26
|
* POST /api/workspaces/<ws>/agents/<id>/remove forget a stopped one (Undo:
|
|
22
27
|
* /api/agents/restore)
|
|
23
28
|
* POST /api/agents/clear-stopped { workspaces: [...] }: forget
|
|
@@ -74,6 +79,7 @@ import {
|
|
|
74
79
|
resumeInterrupted,
|
|
75
80
|
setRuntimePath,
|
|
76
81
|
resolveAgentsDeps,
|
|
82
|
+
relaunchAgent,
|
|
77
83
|
resumeAgent,
|
|
78
84
|
startAgent,
|
|
79
85
|
startFresh,
|
|
@@ -98,7 +104,7 @@ export interface AgentRouteDeps {
|
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
const AGENTS_PATH =
|
|
101
|
-
/^\/api\/workspaces\/([^/]+)\/agents(?:\/([^/]+)\/(stop|resume|fresh|remove))?$/;
|
|
107
|
+
/^\/api\/workspaces\/([^/]+)\/agents(?:\/([^/]+)\/(stop|resume|fresh|relaunch|remove))?$/;
|
|
102
108
|
|
|
103
109
|
const RUNTIME_PATH = /^\/api\/agents\/runtimes\/([^/]+)\/path$/;
|
|
104
110
|
/** A start's body is a few short names. */
|
|
@@ -282,7 +288,13 @@ export async function handleAgentRoutes(
|
|
|
282
288
|
return true;
|
|
283
289
|
}
|
|
284
290
|
const id = m[2] === undefined ? undefined : safeDecode(m[2]);
|
|
285
|
-
const action = m[3] as
|
|
291
|
+
const action = m[3] as
|
|
292
|
+
| "stop"
|
|
293
|
+
| "resume"
|
|
294
|
+
| "fresh"
|
|
295
|
+
| "relaunch"
|
|
296
|
+
| "remove"
|
|
297
|
+
| undefined;
|
|
286
298
|
|
|
287
299
|
if (!action && method === "GET") {
|
|
288
300
|
try {
|
|
@@ -364,6 +376,11 @@ export async function handleAgentRoutes(
|
|
|
364
376
|
what = `start ${s.rec.id} fresh (${s.rec.persona}, ${s.rec.runtime}) in ${ws}`;
|
|
365
377
|
audit(what, s.early ? "exited at once" : undefined);
|
|
366
378
|
sendStarted(res, s);
|
|
379
|
+
} else if (verb === "relaunch") {
|
|
380
|
+
const s = await relaunchAgent(d, id!, ws);
|
|
381
|
+
what = `relaunch ${s.rec.id} from its handoff (${s.why}) in ${ws}`;
|
|
382
|
+
audit(what, s.early ? "exited at once" : undefined);
|
|
383
|
+
sendStarted(res, s);
|
|
367
384
|
} else if (verb === "remove") {
|
|
368
385
|
const removed = await removeAgent(d, id!, ws);
|
|
369
386
|
audit(what);
|
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
* shows only Start fresh, saying so. Clear stopped removes every stopped
|
|
41
41
|
* agent in view. After a restart, one line says how many were interrupted,
|
|
42
42
|
* with Resume all and Review.
|
|
43
|
+
*
|
|
44
|
+
* Session rotation (den#4): an agent whose session rotated (after
|
|
45
|
+
* agents.rotate.after_compactions compactions, or its pull request merged)
|
|
46
|
+
* and whose handoff.md passes the check reads "Ready to relaunch (3
|
|
47
|
+
* compactions)" or "(PR merged)", with Relaunch from handoff as its primary
|
|
48
|
+
* action, then Start fresh and Remove; Resume is not offered. With the
|
|
49
|
+
* handoff missing or failing the check, the row says so and keeps Resume
|
|
50
|
+
* and Start fresh. A running one says "Handoff due" or "Ready to relaunch"
|
|
51
|
+
* as a tag.
|
|
43
52
|
*/
|
|
44
53
|
import { fillStartForm } from "/agent-start.js";
|
|
45
54
|
|
|
@@ -181,6 +190,23 @@ export function stopReason(r) {
|
|
|
181
190
|
return stateText(r.state);
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
/** Why it rotates, in words: "3 compactions", "PR merged" (den#4). */
|
|
194
|
+
function rotationWhy(rot) {
|
|
195
|
+
if (rot.due === "pr-merged") return t("ag.rot.merged");
|
|
196
|
+
return t(
|
|
197
|
+
rot.compactions === 1 ? "ag.rot.compactions.one" : "ag.rot.compactions",
|
|
198
|
+
{
|
|
199
|
+
n: rot.compactions,
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Its rotation, when one is due: "ready" or "due" (den#4). */
|
|
205
|
+
function rotationOf(r) {
|
|
206
|
+
if (!r.rotation?.due) return "";
|
|
207
|
+
return r.rotation.ready ? "ready" : "due";
|
|
208
|
+
}
|
|
209
|
+
|
|
184
210
|
/** A stopped agent's row: its button (it opens an ended pane) and actions. */
|
|
185
211
|
function stoppedHtml(r) {
|
|
186
212
|
const open = st.open.includes(r.id);
|
|
@@ -188,26 +214,47 @@ function stoppedHtml(r) {
|
|
|
188
214
|
const nm = `${esc(name)}${r.persona && r.persona !== r.label ? ` <span class="id">${esc(r.label)}</span>` : ""}`;
|
|
189
215
|
const meta = [r.runtime, r.cwd].filter(Boolean).join(" · ");
|
|
190
216
|
const who = esc(r.agent);
|
|
217
|
+
const rot = rotationOf(r);
|
|
218
|
+
const relaunch =
|
|
219
|
+
rot === "ready"
|
|
220
|
+
? `<button type="button" class="den-btn sm primary" data-act="relaunch" aria-label="${esc(t("ag.relaunchName", { id: r.agent }))}">${esc(t("ag.relaunch"))}</button>`
|
|
221
|
+
: "";
|
|
191
222
|
const resume = r.resumable
|
|
192
223
|
? `<button type="button" class="den-btn sm primary" data-act="resume" aria-label="${esc(t("ag.resumeName", { id: r.agent }))}">${esc(t("ag.resume"))}</button>`
|
|
193
224
|
: "";
|
|
194
225
|
const fresh = `<button type="button" class="den-btn sm" data-act="fresh" aria-label="${esc(t("ag.freshName", { id: r.agent }))}">${esc(t("ag.fresh"))}</button>`;
|
|
195
226
|
const remove = `<button type="button" class="den-btn sm" data-act="remove" aria-label="${esc(t("ag.removeName", { id: r.agent }))}">${esc(t("ag.remove"))}</button>`;
|
|
227
|
+
// A rotation due without a handoff that passes: say so (den#4).
|
|
228
|
+
const handoff =
|
|
229
|
+
rot === "due"
|
|
230
|
+
? `<span class="agcant" data-handoff="${esc(r.rotation.handoff)}">${esc(
|
|
231
|
+
t(
|
|
232
|
+
r.rotation.handoff === "missing"
|
|
233
|
+
? "ag.rot.noHandoff"
|
|
234
|
+
: "ag.rot.badHandoff",
|
|
235
|
+
{ why: rotationWhy(r.rotation) },
|
|
236
|
+
),
|
|
237
|
+
)}</span>`
|
|
238
|
+
: "";
|
|
196
239
|
const cant =
|
|
197
|
-
!r.resumable && r.noResume
|
|
240
|
+
!r.resumable && r.noResume && rot !== "ready"
|
|
198
241
|
? `<span class="agcant" data-no-resume>${esc(r.noResume.startsWith(`${r.runtime} `) ? t("ag.noResume", { runtime: r.runtime }) : r.noResume)}</span>`
|
|
199
242
|
: "";
|
|
243
|
+
const stText =
|
|
244
|
+
rot === "ready"
|
|
245
|
+
? t("ag.rot.ready", { why: rotationWhy(r.rotation) })
|
|
246
|
+
: stateText(r.state);
|
|
200
247
|
return (
|
|
201
|
-
`<div class="agst" data-agent="${who}" data-state="${esc(r.state)}" data-ws="${esc(r.workspace)}">` +
|
|
248
|
+
`<div class="agst" data-agent="${who}" data-state="${esc(r.state)}" data-ws="${esc(r.workspace)}"${rot ? ` data-rotation="${rot}"` : ""}>` +
|
|
202
249
|
`<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="agent" data-state="${esc(r.state)}"` +
|
|
203
250
|
` aria-pressed="${open}"${r.attachable ? "" : ' aria-disabled="true"'}` +
|
|
204
251
|
` aria-label="${esc(t("ag.openName", { id: `${r.workspace} / ${name}` }))}">` +
|
|
205
252
|
`<span class="den-dot ${stateTone(r)}" aria-hidden="true"></span>` +
|
|
206
|
-
`<span class="nm">${nm}</span><span class="st">${esc(
|
|
253
|
+
`<span class="nm">${nm}</span><span class="st">${esc(stText)}</span>` +
|
|
207
254
|
(meta ? `<span class="meta">${esc(meta)}</span>` : "") +
|
|
208
255
|
`<span class="why">${esc(stopReason(r))}</span>` +
|
|
209
256
|
`</button>` +
|
|
210
|
-
`<div class="agacts">${cant}${resume}${fresh}${remove}</div>` +
|
|
257
|
+
`<div class="agacts">${handoff}${cant}${relaunch}${resume}${fresh}${remove}</div>` +
|
|
211
258
|
`</div>`
|
|
212
259
|
);
|
|
213
260
|
}
|
|
@@ -223,9 +270,21 @@ function rowHtml(r) {
|
|
|
223
270
|
? `<span class="wbdg">${esc(t("ag.waiting"))}</span>`
|
|
224
271
|
: `<span class="st">${esc(stateText(r.state))}</span>`;
|
|
225
272
|
const meta = [r.runtime, r.cwd].filter(Boolean).join(" · ");
|
|
226
|
-
const
|
|
227
|
-
|
|
273
|
+
const rot = rotationOf(r);
|
|
274
|
+
const rotTag = rot
|
|
275
|
+
? `<span class="tagx warn" data-rotation="${rot}">${esc(
|
|
276
|
+
t(rot === "ready" ? "ag.rot.ready" : "ag.rot.due", {
|
|
277
|
+
why: rotationWhy(r.rotation),
|
|
278
|
+
}),
|
|
279
|
+
)}</span>`
|
|
280
|
+
: "";
|
|
281
|
+
const revokedTag = r.tokenRevoked
|
|
282
|
+
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
228
283
|
: "";
|
|
284
|
+
const tags =
|
|
285
|
+
rotTag || revokedTag
|
|
286
|
+
? `<span class="agtags">${revokedTag}${rotTag}</span>`
|
|
287
|
+
: "";
|
|
229
288
|
return (
|
|
230
289
|
`<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="${esc(r.kind)}" data-state="${esc(r.state)}"` +
|
|
231
290
|
` aria-pressed="${open}"${r.attachable ? "" : ' aria-disabled="true"'}` +
|
|
@@ -760,18 +819,24 @@ async function stopAgent(p, button) {
|
|
|
760
819
|
const agentPath = (r, verb) =>
|
|
761
820
|
`/api/workspaces/${encodeURIComponent(r.workspace)}/agents/${encodeURIComponent(r.agent)}/${verb}`;
|
|
762
821
|
|
|
763
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* Resume (the same conversation), Start fresh, or Relaunch from handoff
|
|
824
|
+
* (den#4: a new conversation that starts from handoff.md); its pane opens
|
|
825
|
+
* on it.
|
|
826
|
+
*/
|
|
764
827
|
async function restartRow(r, verb, button) {
|
|
765
828
|
if (button) button.disabled = true;
|
|
766
829
|
try {
|
|
767
830
|
const out = await api("POST", agentPath(r, verb));
|
|
768
831
|
window.denShell?.toast?.(
|
|
769
832
|
t(
|
|
770
|
-
verb
|
|
771
|
-
? "ag.
|
|
772
|
-
:
|
|
773
|
-
? "ag.
|
|
774
|
-
:
|
|
833
|
+
verb === "relaunch"
|
|
834
|
+
? "ag.relaunched"
|
|
835
|
+
: verb !== "resume"
|
|
836
|
+
? "ag.startedFresh"
|
|
837
|
+
: out?.fresh
|
|
838
|
+
? "ag.resumedFresh"
|
|
839
|
+
: "ag.resumed",
|
|
775
840
|
{ id: r.agent },
|
|
776
841
|
),
|
|
777
842
|
);
|
|
@@ -1093,7 +1158,7 @@ export async function mountAgentsPage(root) {
|
|
|
1093
1158
|
(x) => x.agent === agent && x.workspace === ws,
|
|
1094
1159
|
);
|
|
1095
1160
|
if (!r) return;
|
|
1096
|
-
if (which === "resume" || which === "fresh")
|
|
1161
|
+
if (which === "resume" || which === "fresh" || which === "relaunch")
|
|
1097
1162
|
void restartRow(r, which, act);
|
|
1098
1163
|
else if (which === "remove") void removeRow(r, act);
|
|
1099
1164
|
return;
|
|
@@ -414,6 +414,18 @@
|
|
|
414
414
|
"ag.fresh": "Empezar de cero",
|
|
415
415
|
"ag.freshName": "Empezar {id} de cero",
|
|
416
416
|
"ag.startedFresh": "{id} empezó de cero",
|
|
417
|
+
"ag.rot.ready": "Listo para relanzar ({why})",
|
|
418
|
+
"ag.rot.due": "Falta el traspaso ({why})",
|
|
419
|
+
"ag.rot.compactions": "{n} compactaciones",
|
|
420
|
+
"ag.rot.compactions.one": "{n} compactación",
|
|
421
|
+
"ag.rot.merged": "PR fusionado",
|
|
422
|
+
"ag.rot.noHandoff":
|
|
423
|
+
"Su sesión rotó ({why}), pero no escribió handoff.md. Reanúdalo para que lo escriba, o empieza de cero.",
|
|
424
|
+
"ag.rot.badHandoff":
|
|
425
|
+
"Su sesión rotó ({why}), pero su handoff.md no está completo. Reanúdalo para arreglarlo, o empieza de cero.",
|
|
426
|
+
"ag.relaunch": "Relanzar desde el traspaso",
|
|
427
|
+
"ag.relaunchName": "Relanzar {id} desde el traspaso",
|
|
428
|
+
"ag.relaunched": "{id} relanzado desde su traspaso",
|
|
417
429
|
"ag.remove": "Quitar",
|
|
418
430
|
"ag.removeName": "Quitar {id}",
|
|
419
431
|
"ag.removed": "{id} quitado",
|
|
@@ -401,6 +401,17 @@
|
|
|
401
401
|
"ag.fresh": "新しく始める",
|
|
402
402
|
"ag.freshName": "{id} を新しく始める",
|
|
403
403
|
"ag.startedFresh": "{id} を新しく始めました",
|
|
404
|
+
"ag.rot.ready": "再起動の準備ができました({why})",
|
|
405
|
+
"ag.rot.due": "引き継ぎが必要です({why})",
|
|
406
|
+
"ag.rot.compactions": "圧縮 {n} 回",
|
|
407
|
+
"ag.rot.merged": "PR がマージされました",
|
|
408
|
+
"ag.rot.noHandoff":
|
|
409
|
+
"セッションは交代しました({why})が、handoff.md がありません。再開して書かせるか、新しく始めてください。",
|
|
410
|
+
"ag.rot.badHandoff":
|
|
411
|
+
"セッションは交代しました({why})が、handoff.md が不完全です。再開して直すか、新しく始めてください。",
|
|
412
|
+
"ag.relaunch": "引き継ぎから再起動",
|
|
413
|
+
"ag.relaunchName": "{id} を引き継ぎから再起動",
|
|
414
|
+
"ag.relaunched": "{id} を引き継ぎから再起動しました",
|
|
404
415
|
"ag.remove": "削除",
|
|
405
416
|
"ag.removeName": "{id} を削除",
|
|
406
417
|
"ag.removed": "{id} を削除しました",
|
|
@@ -534,6 +534,19 @@
|
|
|
534
534
|
"ag.restored": "Back in the list: {ids}",
|
|
535
535
|
"ag.clear": "Clear stopped",
|
|
536
536
|
"ag.noResume": "{runtime} can't resume a conversation",
|
|
537
|
+
// Session rotation (den#4).
|
|
538
|
+
"ag.rot.ready": "Ready to relaunch ({why})",
|
|
539
|
+
"ag.rot.due": "Handoff due ({why})",
|
|
540
|
+
"ag.rot.compactions": "{n} compactions",
|
|
541
|
+
"ag.rot.compactions.one": "{n} compaction",
|
|
542
|
+
"ag.rot.merged": "PR merged",
|
|
543
|
+
"ag.rot.noHandoff":
|
|
544
|
+
"Its session rotated ({why}), but it wrote no handoff.md. Resume it to write one, or start fresh.",
|
|
545
|
+
"ag.rot.badHandoff":
|
|
546
|
+
"Its session rotated ({why}), but its handoff.md is not complete. Resume it to fix it, or start fresh.",
|
|
547
|
+
"ag.relaunch": "Relaunch from handoff",
|
|
548
|
+
"ag.relaunchName": "Relaunch {id} from handoff",
|
|
549
|
+
"ag.relaunched": "Relaunched {id} from its handoff",
|
|
537
550
|
"ag.interrupted": "{n} agents were interrupted when {name} restarted",
|
|
538
551
|
"ag.interrupted.one": "{n} agent was interrupted when {name} restarted",
|
|
539
552
|
"ag.resumeAll": "Resume all",
|
|
@@ -318,6 +318,11 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
318
318
|
"/api/workspaces/:ws/agents/:id/fresh",
|
|
319
319
|
"starts an agent afresh: a runtime in a PTY",
|
|
320
320
|
),
|
|
321
|
+
S(
|
|
322
|
+
["POST"],
|
|
323
|
+
"/api/workspaces/:ws/agents/:id/relaunch",
|
|
324
|
+
"relaunches a rotated agent from its handoff: a runtime in a PTY (den#4)",
|
|
325
|
+
),
|
|
321
326
|
S(
|
|
322
327
|
["POST"],
|
|
323
328
|
"/api/agents/resume-interrupted",
|
|
@@ -400,6 +405,7 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
|
400
405
|
"/api/workspaces/:ws/agents/:id/stop",
|
|
401
406
|
"/api/workspaces/:ws/agents/:id/resume",
|
|
402
407
|
"/api/workspaces/:ws/agents/:id/fresh",
|
|
408
|
+
"/api/workspaces/:ws/agents/:id/relaunch",
|
|
403
409
|
"/api/workspaces/:ws/agents/:id/remove",
|
|
404
410
|
],
|
|
405
411
|
"agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
|
|
@@ -43,6 +43,7 @@ import { getStateDir } from "../../shared/paths.js";
|
|
|
43
43
|
import { product } from "../../shared/product.js";
|
|
44
44
|
import { revokedAgentIds } from "../../shared/actor-token.js";
|
|
45
45
|
import { listRecords } from "../../shared/agents/records.js";
|
|
46
|
+
import type { RotationInfo } from "../../shared/agents/rotation.js";
|
|
46
47
|
import {
|
|
47
48
|
connectHost,
|
|
48
49
|
noHost,
|
|
@@ -153,6 +154,11 @@ export interface ListedSession {
|
|
|
153
154
|
tokenRevoked?: boolean;
|
|
154
155
|
/** An approval is pending (AG-6a; only when the host knows). */
|
|
155
156
|
waiting?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Session rotation (den#4): why it rotates and its handoff.md. `ready`:
|
|
159
|
+
* Relaunch from handoff is its action, not Resume.
|
|
160
|
+
*/
|
|
161
|
+
rotation?: RotationInfo;
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
/** The list, from the host's sessions and the agent records. */
|
|
@@ -176,7 +182,7 @@ export function listedSessions(
|
|
|
176
182
|
const out: ListedSession[] = [];
|
|
177
183
|
const agentSessions = new Set<string>();
|
|
178
184
|
noteEnded(d.stateDir(), mine, records);
|
|
179
|
-
for (const r of agentRows(mine, records)) {
|
|
185
|
+
for (const r of agentRows(mine, records, d.stateDir())) {
|
|
180
186
|
const rec = records.find((x) => x.id === r.agent)!;
|
|
181
187
|
const s = r.session;
|
|
182
188
|
if (s) agentSessions.add(s.ref.id);
|
|
@@ -201,6 +207,7 @@ export function listedSessions(
|
|
|
201
207
|
...(r.stoppedAt ? { stoppedAt: r.stoppedAt } : {}),
|
|
202
208
|
...(r.endedAt ? { endedAt: r.endedAt } : {}),
|
|
203
209
|
...(r.interruptedAt ? { interruptedAt: r.interruptedAt } : {}),
|
|
210
|
+
...(r.rotation?.due ? { rotation: r.rotation } : {}),
|
|
204
211
|
...(revoked.has(rec.id) && s && !s.exited ? { tokenRevoked: true } : {}),
|
|
205
212
|
});
|
|
206
213
|
}
|