@monster-orange/den 0.4.15 → 0.4.16
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 +67 -0
- package/README.md +7 -7
- package/docs/distributions.md +82 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +122 -4
- package/src/shared/agents/den-claude.ts +13 -1
- package/src/shared/agents/records.ts +19 -0
- package/src/shared/agents/runtime.ts +5 -0
- package/src/shared/github-app.ts +7 -0
- package/src/ui/cli/agent-ops.ts +297 -1
- package/src/ui/cli/agents-command.ts +67 -3
- package/src/ui/server/agent-routes.ts +89 -2
- package/src/ui/server/github-routes.ts +1 -1
- package/src/ui/server/index.ts +22 -5
- package/src/ui/server/public/agents-page.js +287 -26
- package/src/ui/server/public/agents.html +1 -0
- package/src/ui/server/public/error-page.js +70 -15
- package/src/ui/server/public/error.html +18 -0
- package/src/ui/server/public/i18n/es.js +36 -5
- package/src/ui/server/public/i18n/ja.js +36 -5
- package/src/ui/server/public/persona-ui.css +11 -0
- package/src/ui/server/public/shell.js +35 -5
- package/src/ui/server/route-classes.ts +6 -0
- package/src/ui/server/session-routes.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,73 @@ All notable changes to Den are documented here. Each release lists the
|
|
|
4
4
|
config and manifest changes a distribution must make to move to it under
|
|
5
5
|
**Migrations**.
|
|
6
6
|
|
|
7
|
+
## [0.4.16] - 2026-11-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Change an agent's persona (AG-18).** **Change persona…** in the
|
|
12
|
+
agent pane's ⋯ menu (running or stopped), and `<cli> agents persona
|
|
13
|
+
<id> <persona> [--model <m>] [--now | --after-turn]`. Den stops the
|
|
14
|
+
session and resumes the same conversation with the new persona's role
|
|
15
|
+
prompt, permission mode and allowed commands; the history, id, name,
|
|
16
|
+
workspace, folder, tab and the model it runs stay. The dialog marks the
|
|
17
|
+
current persona and lists what changes. Mid-turn it never stops a turn
|
|
18
|
+
unasked: "It's working. Change after this turn, or stop it now?" (409
|
|
19
|
+
`busy`); After this turn waits for the turn to end, Stop it now stops
|
|
20
|
+
it. The wait lives in the server's memory: if Den restarts first, the
|
|
21
|
+
change is dropped, the record no longer marks it pending, and the audit
|
|
22
|
+
trail says "persona change cancelled: Den restarted". A person's only: an agent's token is refused (403), and there is no
|
|
23
|
+
MCP tool. Only a runtime that can resume offers it. The record keeps
|
|
24
|
+
`personaChanges`, and the audit trail records each change. Route `POST
|
|
25
|
+
/api/workspaces/<ws>/agents/<id>/persona`.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Bring in keeps the conversation's model, and says what the persona
|
|
30
|
+
changes (AG-17.1c).** It used to switch the conversation to the
|
|
31
|
+
persona's model, and drop MCP servers the conversation had outside Den,
|
|
32
|
+
without a word. Now the model stays the one its last answer came from
|
|
33
|
+
("Keeps <model> from the conversation"; Advanced or `--model` picks
|
|
34
|
+
another), and a short list under the persona says "From now on it works
|
|
35
|
+
as <persona>: its role, its permissions (<mode>), Den's tools and
|
|
36
|
+
guards. Its history stays.", plus "It won't have: <servers>." for MCP
|
|
37
|
+
servers seen in its transcript that Den won't load. The persona of the
|
|
38
|
+
latest agent that worked in its folder is preselected (`suggest`), never
|
|
39
|
+
submitted. The list adds `model`, `lost` and `suggest`; the import takes
|
|
40
|
+
`model` and `version`.
|
|
41
|
+
- **The signed-out page shows Sign in with GitHub once the owner is
|
|
42
|
+
linked, with no reload.** With no owner yet it watches the session
|
|
43
|
+
probe (which now says `github.state` to a signed-out browser), and step 1
|
|
44
|
+
turns done and the button appears as soon as `web owner link` finishes.
|
|
45
|
+
|
|
46
|
+
- **The signed-out page explains Sign in with GitHub (SEC-2a-UX).** It
|
|
47
|
+
shows the same three steps whether or not an owner is linked: on the
|
|
48
|
+
machine Den runs on, run `<cli> web owner link` (or use Settings ›
|
|
49
|
+
Access there); enter the code it shows at github.com/login/device and
|
|
50
|
+
approve; come back and press Sign in with GitHub with the same account.
|
|
51
|
+
Once an owner is linked, step 1 shows as done ("Owner: @login") and the
|
|
52
|
+
button follows the steps. Below them: only the owner's GitHub account
|
|
53
|
+
can sign in this way, anyone else uses Approve this computer, and Learn
|
|
54
|
+
more opens docs/distributions.md, "Signing in with GitHub". English,
|
|
55
|
+
Spanish and Japanese.
|
|
56
|
+
- Another GitHub account is refused with "@login isn't the owner of this
|
|
57
|
+
Den. Only the owner's account can use Sign in with GitHub. Use Approve
|
|
58
|
+
this computer instead." It no longer says to ask the owner to add you:
|
|
59
|
+
a Den has one owner, and no one to add.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- An open agent pane stayed on the old session after Change persona, Resume
|
|
64
|
+
or a rotation, showing "[the session exited (143)]" while the list showed
|
|
65
|
+
the new one running. A pane now follows its agent: it moves to the new
|
|
66
|
+
session in the same slot, keeps its scrollback, and says "Switched to
|
|
67
|
+
<persona>". After a session ends the page looks again every second for
|
|
68
|
+
15 s, so the switch shows within about 2 s.
|
|
69
|
+
|
|
70
|
+
### Migrations
|
|
71
|
+
|
|
72
|
+
- None.
|
|
73
|
+
|
|
7
74
|
## [0.4.15] - 2026-11-07
|
|
8
75
|
|
|
9
76
|
### Added
|
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<picture>
|
|
13
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.
|
|
14
|
-
<img src="docs/images/0.4.
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.16/hero-dark.png">
|
|
14
|
+
<img src="docs/images/0.4.16/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
|
|
15
15
|
</picture>
|
|
16
16
|
|
|
17
17
|
## Set up by your agent
|
|
@@ -38,7 +38,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
|
|
|
38
38
|
yours. Den connects them and turns on good practice from the start.
|
|
39
39
|
|
|
40
40
|
<p align="center">
|
|
41
|
-
<img src="docs/images/0.4.
|
|
41
|
+
<img src="docs/images/0.4.16/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
|
|
42
42
|
</p>
|
|
43
43
|
|
|
44
44
|
## How it helps
|
|
@@ -68,10 +68,10 @@ yours. Den connects them and turns on good practice from the start.
|
|
|
68
68
|
lives in your own repo.
|
|
69
69
|
|
|
70
70
|
<p align="center">
|
|
71
|
-
<img src="docs/images/0.4.
|
|
72
|
-
<img src="docs/images/0.4.
|
|
73
|
-
<img src="docs/images/0.4.
|
|
74
|
-
<img src="docs/images/0.4.
|
|
71
|
+
<img src="docs/images/0.4.16/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
|
|
72
|
+
<img src="docs/images/0.4.16/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
|
|
73
|
+
<img src="docs/images/0.4.16/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
|
|
74
|
+
<img src="docs/images/0.4.16/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
|
|
75
75
|
</p>
|
|
76
76
|
|
|
77
77
|
## Get started
|
package/docs/distributions.md
CHANGED
|
@@ -1687,14 +1687,31 @@ the account's id and login, never a token, and no other account gets in.
|
|
|
1687
1687
|
`web owner unlink`. Settings › Access does the same in the browser
|
|
1688
1688
|
(Link your GitHub account, Unlink): shell-grade actions, for a signed-in
|
|
1689
1689
|
browser and never an agent's token.
|
|
1690
|
-
- **Sign in with GitHub** on the signed-out page shows
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1690
|
+
- **Sign in with GitHub** on the signed-out page shows the same three
|
|
1691
|
+
steps whether or not an owner is linked:
|
|
1692
|
+
1. On the machine Den runs on, run `web owner link`, or use
|
|
1693
|
+
Settings › Access there.
|
|
1694
|
+
2. It shows a code. Enter it at `github.com/login/device` and approve.
|
|
1695
|
+
3. Come back to the signed-out page and press **Sign in with GitHub**,
|
|
1696
|
+
with the same GitHub account.
|
|
1697
|
+
|
|
1698
|
+
Once an owner is linked, step 1 shows as done ("Owner: @<login>") and
|
|
1699
|
+
the button follows the steps; before that there is no button. Below the
|
|
1700
|
+
steps the page says only the owner's GitHub account can sign in this
|
|
1701
|
+
way, and that anyone else uses **Approve this computer**; **Learn more**
|
|
1702
|
+
opens this section. With no owner yet, the page watches (the session
|
|
1703
|
+
probe, `GET /api/session`, says `github.state` to a signed-out
|
|
1704
|
+
browser): once the owner is linked on the machine, step 1 shows done
|
|
1705
|
+
and the button appears, with no reload.
|
|
1706
|
+
|
|
1707
|
+
- Pressing the button shows a code and `github.com/login/device`. The
|
|
1708
|
+
server runs the flow, and when the owner approves it sets the same
|
|
1709
|
+
never-expiring session cookie a one-time link sets, and the page reloads
|
|
1710
|
+
signed in. Another account sees "@<login> isn't the owner of this Den.
|
|
1711
|
+
Only the owner's account can use Sign in with GitHub. Use Approve this
|
|
1712
|
+
computer instead." Den has one owner, so there is no one to add. The
|
|
1713
|
+
token is thrown away either way. At most three sign-ins wait for GitHub
|
|
1714
|
+
at once; a code expires as GitHub says (15 minutes).
|
|
1698
1715
|
- **Settings › Access** lists the linked devices (the browser, how it
|
|
1699
1716
|
signed in, the client address when the edge vouched for one, created and
|
|
1700
1717
|
last seen; "This device" on yours), each with **Revoke**, which ends its session at once (its
|
|
@@ -2180,7 +2197,8 @@ server scoped to its workspace.
|
|
|
2180
2197
|
<cli> agents stop <id> [-w <ws>]
|
|
2181
2198
|
<cli> agents resume <id> [-w <ws>]
|
|
2182
2199
|
<cli> agents import -w <ws> [--runtime <r>] [--json] # conversations to bring in
|
|
2183
|
-
<cli> agents import -w <ws> --session <id> --persona <p>
|
|
2200
|
+
<cli> agents import -w <ws> --session <id> --persona <p> [--model <m>] # bring one in
|
|
2201
|
+
<cli> agents persona <id> <persona> [-w <ws>] [--model <m>] [--now | --after-turn] # change its persona
|
|
2184
2202
|
<cli> agents personas [--json]
|
|
2185
2203
|
<cli> agents doctor [-w <ws>] [--json] # can each runtime run here, and why not
|
|
2186
2204
|
<cli> agents handback-check <file|-> # is a hand-back complete and plain (see Guardrails)
|
|
@@ -2251,6 +2269,24 @@ import -w <ws>` lists them too.
|
|
|
2251
2269
|
Den's token, MCP config, hooks, guards and audit trail apply. It is an
|
|
2252
2270
|
agent like any other: stop, resume, rename. The record notes
|
|
2253
2271
|
`broughtInAt`.
|
|
2272
|
+
- **What the persona changes.** The persona is the role it takes on from
|
|
2273
|
+
now on: its role prompt, its permission mode and allowed commands, and
|
|
2274
|
+
Den's tools and guards. The history stays. The dialog says so under the
|
|
2275
|
+
persona list: "From now on it works as <persona>: its role, its
|
|
2276
|
+
permissions (<mode>), Den's tools and guards. Its history stays."
|
|
2277
|
+
- **The model stays the conversation's own.** Den reads the model of its
|
|
2278
|
+
last answer (`message.model` on the last assistant record, in the ends
|
|
2279
|
+
it reads) and runs it on that: "Keeps <model> from the conversation".
|
|
2280
|
+
Advanced (or `--model`) picks another. With no model in the
|
|
2281
|
+
transcript, the persona's applies.
|
|
2282
|
+
- **The MCP servers it won't have.** An agent runs with Den's MCP only
|
|
2283
|
+
(`--strict-mcp-config`). When the transcript shows tool calls from other
|
|
2284
|
+
servers (`mcp__<server>__…`), the dialog names them: "It won't have:
|
|
2285
|
+
<servers>." The list's `lost` field has them.
|
|
2286
|
+
- **A suggested persona.** The list's `suggest` is the persona of the
|
|
2287
|
+
latest agent of the workspace that worked in the conversation's folder
|
|
2288
|
+
(or a folder inside it, or one containing it), else the workspace's
|
|
2289
|
+
latest agent's. The dialog preselects it; you confirm with Bring in.
|
|
2254
2290
|
- **One writer only.** A conversation written in the last minute is taken
|
|
2255
2291
|
to be still open in another terminal: "Still open in another terminal.
|
|
2256
2292
|
Close it there first" (HTTP 409 `open_elsewhere`). A conversation an
|
|
@@ -2260,8 +2296,43 @@ import -w <ws>` lists them too.
|
|
|
2260
2296
|
- Only den-claude can bring one in for now. Other runtimes say "<runtime>
|
|
2261
2297
|
can't bring in a conversation yet".
|
|
2262
2298
|
- Routes: `GET /api/workspaces/<ws>/conversations[?runtime=<r>]` and `POST
|
|
2263
|
-
/api/workspaces/<ws>/conversations/<id>/import` `{ persona, runtime?, id
|
|
2264
|
-
}`. A person's only, unless MCP `agent_import` is opted
|
|
2299
|
+
/api/workspaces/<ws>/conversations/<id>/import` `{ persona, runtime?, id?,
|
|
2300
|
+
model?, version? }`. A person's only, unless MCP `agent_import` is opted
|
|
2301
|
+
in.
|
|
2302
|
+
|
|
2303
|
+
#### Change an agent's persona
|
|
2304
|
+
|
|
2305
|
+
**Change persona…** (the agent pane's ⋯ menu, running or stopped) and
|
|
2306
|
+
`agents persona <id> <persona>` switch an agent to another persona on the
|
|
2307
|
+
same conversation. Den stops its session and resumes the conversation with
|
|
2308
|
+
the new persona's role prompt, permission mode and allowed commands (the
|
|
2309
|
+
old persona's mode and commands go). The history stays, and so do the
|
|
2310
|
+
agent's id, name, workspace, folder, grants, tab and slot, and the model it
|
|
2311
|
+
runs unless Advanced (`--model`) picks another. The dialog lists the
|
|
2312
|
+
personas with the current one marked, and the same "From now on it works
|
|
2313
|
+
as…" line as Bring in.
|
|
2314
|
+
|
|
2315
|
+
- **Never mid-turn without asking.** While it works (den-claude: its
|
|
2316
|
+
transcript's last record is a tool call, or a prompt or tool result with
|
|
2317
|
+
no answer yet), the change is refused: "It's working. Change after this
|
|
2318
|
+
turn, or stop it now?" (HTTP 409 `busy`). **After this turn**
|
|
2319
|
+
(`--after-turn`, `when: "after-turn"`) waits for the turn to end, up to
|
|
2320
|
+
30 minutes, and then switches (the API answers 202 `pending` at once;
|
|
2321
|
+
the record marks it `pendingPersona` while it waits). The wait lives in
|
|
2322
|
+
the server's memory: if Den restarts first, the change is dropped, the
|
|
2323
|
+
mark is cleared at start, and the audit trail records "persona change
|
|
2324
|
+
cancelled: Den restarted";
|
|
2325
|
+
**Stop it now** (`--now`, `when: "now"`) stops it and switches.
|
|
2326
|
+
- **A person's only.** An agent could widen its own permissions with it,
|
|
2327
|
+
so an agent's token is refused (403), the CLI refuses inside an agent's
|
|
2328
|
+
session, and there is no MCP tool.
|
|
2329
|
+
- Only a runtime that can resume offers it (`personaChange: true` on the
|
|
2330
|
+
agent's row); others can't.
|
|
2331
|
+
- The record keeps each change in `personaChanges` (from, to, when, and
|
|
2332
|
+
who), and the audit trail records "persona <id> in <ws>: <from> → <to>,
|
|
2333
|
+
by <who>".
|
|
2334
|
+
- Route: `POST /api/workspaces/<ws>/agents/<id>/persona` `{ persona, model?,
|
|
2335
|
+
version?, when? }`.
|
|
2265
2336
|
|
|
2266
2337
|
### On the Workspaces page
|
|
2267
2338
|
|
package/package.json
CHANGED
|
@@ -32,6 +32,16 @@ export interface RuntimeConversation {
|
|
|
32
32
|
lastAt: string;
|
|
33
33
|
/** The folder it ran in. */
|
|
34
34
|
cwd: string;
|
|
35
|
+
/**
|
|
36
|
+
* AG-17.1c: the model its last answer came from (the last assistant
|
|
37
|
+
* record's `message.model`), so Bring in keeps it. Absent when unknown.
|
|
38
|
+
*/
|
|
39
|
+
model?: string;
|
|
40
|
+
/**
|
|
41
|
+
* AG-17.1c: the MCP servers its tools came from (`mcp__<server>__<tool>`
|
|
42
|
+
* tool calls in the ends read), sorted. Absent when none.
|
|
43
|
+
*/
|
|
44
|
+
mcp?: string[];
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/** Bytes read at each end of a transcript. */
|
|
@@ -114,22 +124,63 @@ function promptText(rec: Record<string, unknown>): string | null {
|
|
|
114
124
|
return cleanName(text);
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
/** A transcript
|
|
127
|
+
/** A model name as a transcript records it: short, printable, not synthetic. */
|
|
128
|
+
const MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
|
|
129
|
+
/** An MCP tool's name: `mcp__<server>__<tool>`. */
|
|
130
|
+
const MCP_TOOL_RE = /^mcp__([A-Za-z0-9_.-]+?)__[A-Za-z0-9_.-]+$/;
|
|
131
|
+
|
|
132
|
+
/** An assistant record's tool calls' names. */
|
|
133
|
+
function toolNames(rec: Record<string, unknown>): string[] {
|
|
134
|
+
const content = (rec.message as { content?: unknown } | undefined)?.content;
|
|
135
|
+
if (!Array.isArray(content)) return [];
|
|
136
|
+
return content.flatMap((c) => {
|
|
137
|
+
const b = c as { type?: unknown; name?: unknown };
|
|
138
|
+
return b?.type === "tool_use" && typeof b.name === "string" ? [b.name] : [];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A transcript's summary from its two ends (AG-17.1, AG-17.1c). */
|
|
143
|
+
export interface TranscriptSummary {
|
|
144
|
+
cwd: string | null;
|
|
145
|
+
title: string | null;
|
|
146
|
+
/** The last assistant record's model, or null. */
|
|
147
|
+
model: string | null;
|
|
148
|
+
/** The MCP servers its tool calls name, sorted. */
|
|
149
|
+
mcp: string[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A transcript's cwd, title, last model and MCP servers, from its two ends. */
|
|
118
153
|
export function transcriptSummary(
|
|
119
154
|
file: string,
|
|
120
155
|
bytes = ENDS_BYTES,
|
|
121
|
-
):
|
|
156
|
+
): TranscriptSummary {
|
|
122
157
|
const { head, tail } = readEnds(file, bytes);
|
|
123
158
|
let cwd: string | null = null;
|
|
124
159
|
let prompt: string | null = null;
|
|
125
160
|
let title: string | null = null;
|
|
161
|
+
let model: string | null = null;
|
|
162
|
+
const mcp = new Set<string>();
|
|
126
163
|
const titleIn = (line: string) => {
|
|
127
164
|
if (!line.includes("custom-title")) return;
|
|
128
165
|
const r = parse(line);
|
|
129
166
|
if (r?.type === "custom-title") title = cleanName(r.customTitle) ?? title;
|
|
130
167
|
};
|
|
168
|
+
// AG-17.1c: the last answer's model, and the MCP servers its tools came from.
|
|
169
|
+
const assistantIn = (line: string) => {
|
|
170
|
+
if (!line.includes('"assistant"')) return;
|
|
171
|
+
const r = parse(line);
|
|
172
|
+
if (r?.type !== "assistant") return;
|
|
173
|
+
const m = (r.message as { model?: unknown } | undefined)?.model;
|
|
174
|
+
if (typeof m === "string" && MODEL_RE.test(m) && m !== "<synthetic>")
|
|
175
|
+
model = m;
|
|
176
|
+
for (const n of toolNames(r)) {
|
|
177
|
+
const hit = MCP_TOOL_RE.exec(n);
|
|
178
|
+
if (hit) mcp.add(hit[1]);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
131
181
|
for (const line of head) {
|
|
132
182
|
titleIn(line);
|
|
183
|
+
assistantIn(line);
|
|
133
184
|
if (cwd && prompt) continue;
|
|
134
185
|
if (!line.includes('"cwd"') && !line.includes('"user"')) continue;
|
|
135
186
|
const r = parse(line);
|
|
@@ -138,8 +189,73 @@ export function transcriptSummary(
|
|
|
138
189
|
cwd = r.cwd;
|
|
139
190
|
prompt ??= promptText(r);
|
|
140
191
|
}
|
|
141
|
-
for (const line of tail)
|
|
142
|
-
|
|
192
|
+
for (const line of tail) {
|
|
193
|
+
titleIn(line);
|
|
194
|
+
assistantIn(line);
|
|
195
|
+
}
|
|
196
|
+
return { cwd, title: title ?? prompt, model, mcp: [...mcp].sort() };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Written this recently, a turn whose last record is an answer is still going. */
|
|
200
|
+
export const TURN_QUIET_MS = 5_000;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* AG-18: whether a conversation's turn is still going, from its tail: the
|
|
204
|
+
* last user or assistant record. An answer that ended (`end_turn`, a stop
|
|
205
|
+
* sequence, the token limit) is idle; a tool call, or a prompt or tool
|
|
206
|
+
* result with no answer yet, is busy; an answer with no stop reason is busy
|
|
207
|
+
* until the file has been quiet TURN_QUIET_MS. An interrupted turn is idle.
|
|
208
|
+
* No transcript: idle (nothing was asked yet).
|
|
209
|
+
*/
|
|
210
|
+
export function transcriptBusy(
|
|
211
|
+
file: string | null,
|
|
212
|
+
now = Date.now(),
|
|
213
|
+
bytes = ENDS_BYTES,
|
|
214
|
+
): boolean {
|
|
215
|
+
if (!file) return false;
|
|
216
|
+
let mtime: number;
|
|
217
|
+
try {
|
|
218
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const { head, tail } = readEnds(file, bytes);
|
|
223
|
+
const lines = tail.length ? [...head, ...tail] : head;
|
|
224
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
225
|
+
const line = lines[i];
|
|
226
|
+
if (!line.includes('"user"') && !line.includes('"assistant"')) continue;
|
|
227
|
+
const r = parse(line);
|
|
228
|
+
if (!r || r.isSidechain === true || r.isMeta === true) continue;
|
|
229
|
+
if (r.type === "assistant") {
|
|
230
|
+
const msg = r.message as { stop_reason?: unknown } | undefined;
|
|
231
|
+
const stop = msg?.stop_reason;
|
|
232
|
+
if (stop === "tool_use" || toolNames(r).length) return true;
|
|
233
|
+
if (
|
|
234
|
+
stop === "end_turn" ||
|
|
235
|
+
stop === "stop_sequence" ||
|
|
236
|
+
stop === "max_tokens"
|
|
237
|
+
)
|
|
238
|
+
return false;
|
|
239
|
+
return now - mtime < TURN_QUIET_MS;
|
|
240
|
+
}
|
|
241
|
+
if (r.type === "user") {
|
|
242
|
+
const content = (r.message as { content?: unknown } | undefined)?.content;
|
|
243
|
+
const text =
|
|
244
|
+
typeof content === "string"
|
|
245
|
+
? content
|
|
246
|
+
: Array.isArray(content)
|
|
247
|
+
? ((
|
|
248
|
+
content.find(
|
|
249
|
+
(c) => (c as { type?: unknown })?.type === "text",
|
|
250
|
+
) as { text?: unknown } | undefined
|
|
251
|
+
)?.text ?? "")
|
|
252
|
+
: "";
|
|
253
|
+
if (typeof text === "string" && text.startsWith("[Request interrupted"))
|
|
254
|
+
return false;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
143
259
|
}
|
|
144
260
|
|
|
145
261
|
/** A folder's absolute forms: as given and its real path. */
|
|
@@ -236,6 +352,8 @@ export function claudeConversations(
|
|
|
236
352
|
title: s.title,
|
|
237
353
|
lastAt: new Date(f.mtimeMs).toISOString(),
|
|
238
354
|
cwd,
|
|
355
|
+
...(s.model ? { model: s.model } : {}),
|
|
356
|
+
...(s.mcp.length ? { mcp: s.mcp } : {}),
|
|
239
357
|
});
|
|
240
358
|
}
|
|
241
359
|
return out;
|
|
@@ -70,7 +70,11 @@ import { catalogFind } from "./catalog-runtime.js";
|
|
|
70
70
|
import { locateRuntime, type RuntimeFind } from "./runtime-search.js";
|
|
71
71
|
import { claudeProjectsDir } from "../usage/paths.js";
|
|
72
72
|
import { cleanName, transcriptTitle } from "./transcript-follow.js";
|
|
73
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
claudeConversations,
|
|
75
|
+
transcriptBusy,
|
|
76
|
+
transcriptSummary,
|
|
77
|
+
} from "./conversations.js";
|
|
74
78
|
|
|
75
79
|
/**
|
|
76
80
|
* `/rename "x"` keeps the quotes in customTitle: one matching pair around
|
|
@@ -507,6 +511,14 @@ export const denClaude: Runtime = {
|
|
|
507
511
|
return null;
|
|
508
512
|
}
|
|
509
513
|
},
|
|
514
|
+
// AG-18: mid-turn, from its transcript's tail (never the whole file).
|
|
515
|
+
turnBusy(token, env = process.env) {
|
|
516
|
+
try {
|
|
517
|
+
return transcriptBusy(transcriptFile(token, env));
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
},
|
|
510
522
|
// AG-4b, unofficial (catalog sessionName): `/rename` appends a
|
|
511
523
|
// {"type":"custom-title","customTitle":…} record to the transcript; the
|
|
512
524
|
// latest one, read on from the last ask (never the whole file), less one
|
|
@@ -103,6 +103,25 @@ export interface AgentRecord {
|
|
|
103
103
|
* the list tags it. Start fresh and Relaunch keep the choices.
|
|
104
104
|
*/
|
|
105
105
|
overrides?: Override[];
|
|
106
|
+
/**
|
|
107
|
+
* AG-18: each persona change, oldest first: from, to, when, and who (a
|
|
108
|
+
* person's sign-in, or "cli").
|
|
109
|
+
*/
|
|
110
|
+
personaChanges?: PersonaChange[];
|
|
111
|
+
/**
|
|
112
|
+
* AG-18: a persona change waiting for this agent's turn to end. The wait
|
|
113
|
+
* lives in the server's memory, so a restart drops it; the upkeep at
|
|
114
|
+
* start clears this and audits the cancel (cancelStalePersonaChanges).
|
|
115
|
+
*/
|
|
116
|
+
pendingPersona?: { to: string; since: string; by?: string };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One persona change of an agent (AG-18). */
|
|
120
|
+
export interface PersonaChange {
|
|
121
|
+
from: string;
|
|
122
|
+
to: string;
|
|
123
|
+
at: string;
|
|
124
|
+
by?: string;
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
/** A setting a session can override for one launch (AG-10.9). */
|
|
@@ -207,6 +207,11 @@ export interface Runtime {
|
|
|
207
207
|
): RuntimeConversation[];
|
|
208
208
|
/** AG-17.1: the folder conversation `token` ran in, or null. Never throws. */
|
|
209
209
|
conversationCwd?(token: string, env?: NodeJS.ProcessEnv): string | null;
|
|
210
|
+
/**
|
|
211
|
+
* AG-18: whether conversation `token` is mid-turn (den-claude: its
|
|
212
|
+
* transcript's tail). None: the runtime can't tell. Never throws.
|
|
213
|
+
*/
|
|
214
|
+
turnBusy?(token: string, env?: NodeJS.ProcessEnv): boolean;
|
|
210
215
|
/**
|
|
211
216
|
* What the runtime prints when it exits at once (0.4.8): `noConversation`
|
|
212
217
|
* when a resume finds nothing to continue, `idInUse` when a new
|
package/src/shared/github-app.ts
CHANGED
|
@@ -83,6 +83,13 @@ export function githubClientId(configured: string | undefined): string {
|
|
|
83
83
|
return (configured ?? MO_DEN_GITHUB_CLIENT_ID).trim();
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The docs section the signed-out page's Learn more opens (SEC-2a-UX):
|
|
88
|
+
* docs/distributions.md, "Signing in with GitHub", in the public repo.
|
|
89
|
+
*/
|
|
90
|
+
export const GITHUB_SIGNIN_DOCS =
|
|
91
|
+
"https://github.com/Monster-Orange/den/blob/main/docs/distributions.md#signing-in-with-github";
|
|
92
|
+
|
|
86
93
|
/** What a GitHub button says while no client id is configured. */
|
|
87
94
|
export function noClientIdMessage(cli: string): string {
|
|
88
95
|
return (
|