@monster-orange/den 0.4.16 → 0.4.18
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 +101 -0
- package/README.md +7 -7
- package/docs/distributions.md +94 -1
- package/package.json +1 -1
- package/src/ui/cli/agent-ops.ts +50 -0
- package/src/ui/cli/agents-command.ts +5 -5
- package/src/ui/server/agent-routes.ts +13 -1
- package/src/ui/server/check-pool.ts +72 -0
- package/src/ui/server/index.ts +105 -10
- package/src/ui/server/public/agents-page.js +98 -16
- package/src/ui/server/public/agents.html +22 -0
- package/src/ui/server/public/environments.html +223 -140
- package/src/ui/server/public/graph.css +40 -0
- package/src/ui/server/public/graph.js +47 -6
- package/src/ui/server/public/header.js +7 -0
- package/src/ui/server/public/i18n/es.js +46 -0
- package/src/ui/server/public/i18n/ja.js +46 -0
- package/src/ui/server/public/index.html +6 -0
- package/src/ui/server/public/local-dev.html +199 -27
- package/src/ui/server/public/log-source.js +26 -0
- package/src/ui/server/public/logs.html +7 -20
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/release.html +19 -1
- package/src/ui/server/public/services.html +18 -3
- package/src/ui/server/public/shell.js +360 -32
- package/src/ui/server/public/svc-panel.js +672 -0
- package/src/ui/server/public/topbar.js +34 -1
- package/src/ui/server/public/view-scope.js +61 -0
- package/src/ui/server/public/workspace-picker.js +2 -0
- package/src/ui/server/route-classes.ts +2 -0
- package/src/ui/server/run-history.ts +181 -0
- package/src/ui/server/session-routes.ts +94 -17
- package/src/ui/server/ui-assets.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,107 @@ 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.18] - 2026-11-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **A pinned service opens a quick panel, not a page jump.** A click on a
|
|
12
|
+
pinned service's name in the rail (or Enter on its row, or its dot in
|
|
13
|
+
the collapsed rail) opens a panel beside the row, without leaving the
|
|
14
|
+
page. It has the service's workspace, state and URL; Visit ↗, Start or
|
|
15
|
+
Stop, Restart and Logs, which call what the Services page calls; the
|
|
16
|
+
repo's last test run with Run tests; the agents working in the repo,
|
|
17
|
+
each with Open, and Start agent here; and Open in Services, Open in
|
|
18
|
+
Graph and Unpin. It stays current while open. Esc or a click outside
|
|
19
|
+
closes it and focus goes back to the row. Under 640 px it is a bottom
|
|
20
|
+
sheet. ⌘-click or a middle click still opens Services in a new tab. The
|
|
21
|
+
Services card's name and the Graph card's new ⋯ open the same panel.
|
|
22
|
+
- **Visit ↗ on every pinned row.** Always shown, it opens the service's
|
|
23
|
+
URL in a new tab. It is greyed out ("Not running") while the service is
|
|
24
|
+
down, and absent when the service has no URL. The hover × is gone:
|
|
25
|
+
Unpin is in the panel, and Delete on the row still unpins.
|
|
26
|
+
- **Start agent here.** The Start dialog has a Folder field (optional):
|
|
27
|
+
the agent runs in that folder of the workspace's, or in a repo's
|
|
28
|
+
folder. The route takes it as `folder`, and refuses a folder outside
|
|
29
|
+
the workspace (403). The panel's Start agent here fills the workspace
|
|
30
|
+
and the repo's folder.
|
|
31
|
+
- **Open in Graph focuses the service.** `/graph#focus=<repo>` or
|
|
32
|
+
`#focus=<repo>/<service>` opens the Graph on that card.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **The panel's Logs reads the service's log.** It asks where the Logs
|
|
37
|
+
page asks (taillog for a local service), so a service with no `logs:`
|
|
38
|
+
block no longer says "Connection closed". The Services page's Logs
|
|
39
|
+
uses the same rule.
|
|
40
|
+
- **"starting…" follows health.** After Start or Restart, the panel
|
|
41
|
+
leaves "starting…" once health says up, once it says down after a
|
|
42
|
+
moment, or after two minutes. Restart works again then, even when the
|
|
43
|
+
service's `up` never exits (a dev server).
|
|
44
|
+
- **A pinned row's Visit ↗ greys out with the panel.** It reads the
|
|
45
|
+
panel's live state, so it greys out on the same health update, also
|
|
46
|
+
for a service outside the top bar's View.
|
|
47
|
+
- **Open in Graph keeps its target.** The Graph page keeps the URL's
|
|
48
|
+
`#focus=` when it picks a workspace, so the card is focused and in view.
|
|
49
|
+
|
|
50
|
+
## [0.4.17] - 2026-11-08
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **An agent's workspace is a link.** The agent pane's header and the
|
|
55
|
+
workspace's heading in the Agents list link to the Workspaces page
|
|
56
|
+
focused on it (`?ws=`). ⌘K has "Go to workspace <ws>" for every
|
|
57
|
+
workspace, the front agent's first on the Agents page, found by the
|
|
58
|
+
workspace's name or any of its agents'.
|
|
59
|
+
- **"Last test run" is the server's.** The Workspaces page's "Last run"
|
|
60
|
+
column is now "Last test run", read from the server's run history
|
|
61
|
+
(`<state>/run-history.jsonl`, `GET /api/runs/last`). It shows runs from
|
|
62
|
+
every browser and survives a restart. The service's own state ("up 2h")
|
|
63
|
+
is shown apart from it.
|
|
64
|
+
- **One org rule.** Environments and Releases have no org picker of their
|
|
65
|
+
own. Both follow the top bar's View and say so with the same note. A
|
|
66
|
+
page shows its own org picker only when it filters by org.
|
|
67
|
+
- **Environments never waits on a slow host.** Every row shows at once,
|
|
68
|
+
pulsing "checking", and each result streams into its row. The server
|
|
69
|
+
runs at most 12 checks at once, each capped at 10 s; one that runs out
|
|
70
|
+
of time says "timed out after 10 s". Re-check works while checks run.
|
|
71
|
+
- **An agent in an inactive workspace opens its terminal.** The terminal
|
|
72
|
+
route checks the person and the agent, not the workspace's active
|
|
73
|
+
flag. The pane says "Sign in" only when you're signed out; any other
|
|
74
|
+
refusal gives its real reason.
|
|
75
|
+
- **Menus are never clipped.** Every ⋯ menu in the shell (Workspaces
|
|
76
|
+
rows and the workspace menu, the top bar's menus) now opens in the
|
|
77
|
+
browser's top layer through one shared helper, so a scrolling table or
|
|
78
|
+
card no longer cuts it off. It stays beside its button, flips up near
|
|
79
|
+
the bottom of the window, and closes on an outside click as before.
|
|
80
|
+
- **The Tests module picker is never clipped.** It uses the same
|
|
81
|
+
top-layer menu.
|
|
82
|
+
- **Visit ↗ opens a service.** Workspaces (beside Start, Stop, Restart
|
|
83
|
+
and Logs, and as a link on the service's name), Services and Graph
|
|
84
|
+
cards open the service's edge URL when it is routed, else its local
|
|
85
|
+
URL, in a new tab. A service with no URL has no Visit. Below 1600 px
|
|
86
|
+
wide the Workspaces button shows only "↗". The detail panel's "Open ↗"
|
|
87
|
+
is now "Visit ↗".
|
|
88
|
+
- **Favourite a service from Workspaces and Graph.** Workspaces rows and
|
|
89
|
+
Graph cards (top-right) have the ☆ star. It uses the same list as
|
|
90
|
+
Services, so a favourite set on one page shows on all of them.
|
|
91
|
+
- **Change persona on an agent that was never prompted** said "same
|
|
92
|
+
conversation", though there was nothing to resume: it starts fresh under
|
|
93
|
+
the same conversation id, as Resume does. The CLI, the API (`note`, with
|
|
94
|
+
`fresh: true`), the toast and the pane now say "nothing to resume yet;
|
|
95
|
+
started fresh (same conversation id, folder and scope)".
|
|
96
|
+
- **Last test run counts a repo's own suites.** A run of the suites pillar
|
|
97
|
+
(a repo's `tests:`, such as `unit`) and a repo's performance modules
|
|
98
|
+
now name the repos they test, so a repo with only `tests: unit` fills
|
|
99
|
+
its Last test run. Den's own integration tests name none.
|
|
100
|
+
- **The Workspaces table keeps its height in a short window.** Under about
|
|
101
|
+
620 px tall it shrank to nothing and its rows and ⋯ were out of reach.
|
|
102
|
+
It keeps at least 200 px and scrolls.
|
|
103
|
+
|
|
104
|
+
### Migrations
|
|
105
|
+
|
|
106
|
+
None.
|
|
107
|
+
|
|
7
108
|
## [0.4.16] - 2026-11-08
|
|
8
109
|
|
|
9
110
|
### 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.18/hero-dark.png">
|
|
14
|
+
<img src="docs/images/0.4.18/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.18/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.18/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.18/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.18/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.18/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
|
@@ -355,6 +355,52 @@ is never mistaken for another repo's service. An alias that names more than
|
|
|
355
355
|
one service is an error that lists them (`"worker" is ambiguous in workspace
|
|
356
356
|
"default": one/worker, two/worker`); name one of them.
|
|
357
357
|
|
|
358
|
+
## Opening a service
|
|
359
|
+
|
|
360
|
+
A service with a URL gets **Visit ↗**, which opens it in a new tab. It
|
|
361
|
+
shows on Workspaces (beside Start, Stop, Restart and Logs, and as a link
|
|
362
|
+
on the service's name), on Services and on Graph cards. Den picks the
|
|
363
|
+
URL in this order: the service's `urls.<env>`; for `local`, its edge
|
|
364
|
+
route when the edge serves one; else the origin of its HTTP health
|
|
365
|
+
check. A service with none of these has no Visit.
|
|
366
|
+
|
|
367
|
+
The ☆ star on Workspaces rows, Services and Graph cards marks a
|
|
368
|
+
favourite. It is one list, saved in `shell.pins`, so a star set on one
|
|
369
|
+
page shows on all of them.
|
|
370
|
+
|
|
371
|
+
### The service panel
|
|
372
|
+
|
|
373
|
+
A pinned service in the rail has **↗** at the end of its row, always
|
|
374
|
+
shown: it opens the service's URL in a new tab ("Visit <service> (opens a
|
|
375
|
+
new tab)", the URL as its tooltip). It is greyed out, saying "Not
|
|
376
|
+
running", while the service is down, and absent when the service has no
|
|
377
|
+
URL.
|
|
378
|
+
|
|
379
|
+
A click on the service's name (or Enter on its row, or its dot when the
|
|
380
|
+
rail is collapsed) opens its **panel** beside the row, without leaving
|
|
381
|
+
the page. ⌘-click, Ctrl-click or a middle click still opens Services in a
|
|
382
|
+
new tab. Esc or a click outside closes it, and focus goes back to the
|
|
383
|
+
row. Under 640 px wide it is a sheet at the bottom of the screen. The
|
|
384
|
+
Services card's name and the Graph card's **⋯** open the same panel.
|
|
385
|
+
|
|
386
|
+
Top to bottom, it has:
|
|
387
|
+
|
|
388
|
+
- the name, workspace, health dot, state ("up 2h", "down: HTTP 502",
|
|
389
|
+
"starting…") and URL;
|
|
390
|
+
- **Visit ↗**, then **Start** or **Stop**, **Restart** and **Logs**. These
|
|
391
|
+
call what the Services page calls (`/api/service/exec` with `up`,
|
|
392
|
+
`down` or `restart`, and `/api/service/logs`), each in a tab of the runs
|
|
393
|
+
dock. Logs opens the dock on that service;
|
|
394
|
+
- **Last test run** (the server's run history, `GET /api/runs/last`) and
|
|
395
|
+
**Run tests**, which runs the repo's suites (`/api/run?pillar=suites`);
|
|
396
|
+
- **Agents**: the agents working in the repo, each with **Open**, and
|
|
397
|
+
**+ Start agent here**, which opens the Start dialog with the workspace
|
|
398
|
+
and the repo's folder filled in;
|
|
399
|
+
- **Open in Services**, **Open in Graph** and **Unpin** (or **Pin**).
|
|
400
|
+
|
|
401
|
+
It stays current while it is open, from the same feeds the pages use. Delete
|
|
402
|
+
on a pinned row still unpins it.
|
|
403
|
+
|
|
358
404
|
## Health checks
|
|
359
405
|
|
|
360
406
|
A service declares its health per environment under `health`, in one of
|
|
@@ -432,6 +478,36 @@ that a container existed, running or not).
|
|
|
432
478
|
`service`, with both forms at once, or with a key of another type (`cmd`,
|
|
433
479
|
`url`), and says which.
|
|
434
480
|
|
|
481
|
+
### The Environments page
|
|
482
|
+
|
|
483
|
+
The Environments page lists every service's row at once, each cell pulsing
|
|
484
|
+
"checking" until its result arrives. The results stream in one at a time
|
|
485
|
+
(`GET /api/health/stream`, one event per check), so a slow host holds up
|
|
486
|
+
its own cell only. The server runs at most 12 checks at once, and no check
|
|
487
|
+
takes longer than 10 s. A check that runs out of time says "timed out after
|
|
488
|
+
10 s" in its row. Re-check and the page's other controls work while the
|
|
489
|
+
checks run.
|
|
490
|
+
|
|
491
|
+
The page has no org picker of its own. It follows the top bar's View (all
|
|
492
|
+
active workspaces, or one), asks for that workspace only, and says so:
|
|
493
|
+
"Shows workspace south, as the top bar's View. Change it there." The
|
|
494
|
+
Release page follows the same rule with the same note. A page shows its own
|
|
495
|
+
org picker only when it filters by org.
|
|
496
|
+
|
|
497
|
+
### Last test run
|
|
498
|
+
|
|
499
|
+
The Workspaces page's "Last test run" column shows each repo's latest test
|
|
500
|
+
run: its result (✓ or ✗), how long ago, and the pillar and environment on
|
|
501
|
+
hover. It reads the server's run history, so it shows runs started from any
|
|
502
|
+
browser or tab, and it survives a restart. The service's own state ("up 2h")
|
|
503
|
+
is shown apart from it, in the Health cell.
|
|
504
|
+
|
|
505
|
+
The history is `<state>/run-history.jsonl` (mode 0600): one line when a run
|
|
506
|
+
starts and one when it ends. Past 2000 lines it keeps the newest 1000.
|
|
507
|
+
`GET /api/runs/last` (read only) gives `{ repos: { <repo>: { id, pillar,
|
|
508
|
+
env, repos, startedAt, endedAt, exitCode, running } } }`. A run a restart
|
|
509
|
+
cut off has no end and is not shown.
|
|
510
|
+
|
|
435
511
|
## Placement
|
|
436
512
|
|
|
437
513
|
Each service says where it runs, per environment: `local` (on this machine,
|
|
@@ -2355,6 +2431,11 @@ started. The workspace header counts them ("2 agents").
|
|
|
2355
2431
|
`agents.runtimes.<id>.bin` to its path in <your user.yml>, or run
|
|
2356
2432
|
`<cli> agents doctor`" (see [Runtimes](#runtimes)). Nothing fails only
|
|
2357
2433
|
after Start.
|
|
2434
|
+
- **Folder** (optional) is where the agent runs: a folder inside the
|
|
2435
|
+
workspace's own, or a repo's folder of the workspace (relative to the
|
|
2436
|
+
workspace's folder, or absolute). Empty, it runs in the workspace's
|
|
2437
|
+
folder. A folder outside the workspace is refused (403). The service
|
|
2438
|
+
panel's **Start agent here** fills it with the repo's folder.
|
|
2358
2439
|
- The page and the CLI call the same operations and read the same agent
|
|
2359
2440
|
records, so an agent started in one is listed in the other. A workspace
|
|
2360
2441
|
lists its own agents only.
|
|
@@ -2369,7 +2450,7 @@ started. The workspace header counts them ("2 agents").
|
|
|
2369
2450
|
|
|
2370
2451
|
The routes: `GET /api/workspaces/<ws>/agents`, `POST
|
|
2371
2452
|
/api/workspaces/<ws>/agents` (`{ persona, runtime?, model?, version?,
|
|
2372
|
-
permission?, allow?, instruction?, id? }`, see
|
|
2453
|
+
permission?, allow?, instruction?, id?, folder? }`, see
|
|
2373
2454
|
[The Start dialog](#the-start-dialog)), `POST
|
|
2374
2455
|
/api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, `POST
|
|
2375
2456
|
/api/workspaces/<ws>/sessions` (a plain session), `GET /api/personas`
|
|
@@ -2552,6 +2633,18 @@ text frame `{"type":"resize","cols":c,"rows":r}` resizes, and the server
|
|
|
2552
2633
|
sends `{"type":"session",…}` first and `{"type":"exit","exitCode":n}` when
|
|
2553
2634
|
the session ends.
|
|
2554
2635
|
|
|
2636
|
+
The terminal route checks the person and the agent, not whether the
|
|
2637
|
+
agent's workspace is active: an agent in a workspace you've switched away
|
|
2638
|
+
from still opens. A `GET` of the io path (not a WebSocket) says why one
|
|
2639
|
+
would be refused: `401` when you're signed out (the pane then says "Sign
|
|
2640
|
+
in"), `404` with the reason when there is no such session here, and `426`
|
|
2641
|
+
when the upgrade would be allowed. The pane shows that reason, not "Sign
|
|
2642
|
+
in", for any refusal but the first.
|
|
2643
|
+
|
|
2644
|
+
The pane's header and the workspace's heading in the list link to the
|
|
2645
|
+
Workspaces page focused on that workspace (`/local-dev?ws=<ws>`), and ⌘K
|
|
2646
|
+
has "Go to workspace <ws>" for each workspace with agents.
|
|
2647
|
+
|
|
2555
2648
|
### Stopped agents: Resume, Start fresh, Remove
|
|
2556
2649
|
|
|
2557
2650
|
An agent that stops stays in the list, below the running ones (AG-14, the
|
package/package.json
CHANGED
package/src/ui/cli/agent-ops.ts
CHANGED
|
@@ -1409,6 +1409,12 @@ export interface StartRequest {
|
|
|
1409
1409
|
allow?: unknown;
|
|
1410
1410
|
/** AG-10.10: a display name for this agent (AG-4b's), to tell two apart. */
|
|
1411
1411
|
name?: unknown;
|
|
1412
|
+
/**
|
|
1413
|
+
* 0.4.18, Start agent here: the folder it runs in, the workspace's own
|
|
1414
|
+
* or one inside it (a repo's), relative to the workspace's folder or
|
|
1415
|
+
* absolute. Default: the workspace's folder.
|
|
1416
|
+
*/
|
|
1417
|
+
folder?: string;
|
|
1412
1418
|
/**
|
|
1413
1419
|
* The page's start (AG-10.1): a model must be one the runtime offers,
|
|
1414
1420
|
* never a free string (the CLI's --model for generic stays free).
|
|
@@ -1524,6 +1530,39 @@ interface StartMore {
|
|
|
1524
1530
|
bringIn?: { token: string; cwd: string };
|
|
1525
1531
|
}
|
|
1526
1532
|
|
|
1533
|
+
/**
|
|
1534
|
+
* 0.4.18: the folder a start runs in. Relative to the workspace's folder,
|
|
1535
|
+
* or absolute; it must be a directory inside the workspace's scope (its
|
|
1536
|
+
* folder, or a by-path repo's), never outside it.
|
|
1537
|
+
*/
|
|
1538
|
+
export function startFolder(
|
|
1539
|
+
d: AgentsResolved,
|
|
1540
|
+
ws: string,
|
|
1541
|
+
folder: string,
|
|
1542
|
+
): string {
|
|
1543
|
+
const where = "agents start";
|
|
1544
|
+
if (typeof folder !== "string" || !folder.trim() || folder.length > 4096)
|
|
1545
|
+
throw usage(`${where}: folder must be a path`);
|
|
1546
|
+
const dir = path.resolve(d.workspaceDir(ws), folder.trim());
|
|
1547
|
+
let real: string;
|
|
1548
|
+
try {
|
|
1549
|
+
real = fs.realpathSync(dir);
|
|
1550
|
+
} catch {
|
|
1551
|
+
throw usage(`${where}: folder ${dir} does not exist`, 404);
|
|
1552
|
+
}
|
|
1553
|
+
if (!fs.statSync(real).isDirectory())
|
|
1554
|
+
throw usage(`${where}: folder ${dir} is not a directory`);
|
|
1555
|
+
// The real folder only: a link inside the workspace that points out of
|
|
1556
|
+
// it is outside it (the scope's own folders are compared real, too).
|
|
1557
|
+
if (!inScope(real, d.workspaceFolders(ws)))
|
|
1558
|
+
throw refused(
|
|
1559
|
+
`${where}: folder ${dir} is not inside workspace ${ws}`,
|
|
1560
|
+
[],
|
|
1561
|
+
403,
|
|
1562
|
+
);
|
|
1563
|
+
return real;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1527
1566
|
export async function startAgent(
|
|
1528
1567
|
d: AgentsResolved,
|
|
1529
1568
|
req: StartRequest,
|
|
@@ -1571,6 +1610,10 @@ export async function startAgent(
|
|
|
1571
1610
|
...(rt.permissions ? { allow } : {}),
|
|
1572
1611
|
}),
|
|
1573
1612
|
...(more.firstPrompt ? { firstPrompt: more.firstPrompt } : {}),
|
|
1613
|
+
// 0.4.18: Start agent here: in a folder of the workspace's.
|
|
1614
|
+
...(req.folder !== undefined && !more.bringIn
|
|
1615
|
+
? { worktree: startFolder(d, ws, req.folder) }
|
|
1616
|
+
: {}),
|
|
1574
1617
|
// AG-17.1: the runtime's resume, in the folder the conversation ran in.
|
|
1575
1618
|
...(more.bringIn
|
|
1576
1619
|
? { worktree: more.bringIn.cwd, resumeToken: more.bringIn.token }
|
|
@@ -2212,6 +2255,13 @@ export interface PersonaChanged extends Resumed {
|
|
|
2212
2255
|
from: string;
|
|
2213
2256
|
}
|
|
2214
2257
|
|
|
2258
|
+
/**
|
|
2259
|
+
* What a resume with nothing to continue yet says (AG-14, AG-18): the
|
|
2260
|
+
* runtime started fresh under the same conversation id.
|
|
2261
|
+
*/
|
|
2262
|
+
export const FRESH_NOTE =
|
|
2263
|
+
"nothing to resume yet; started fresh (same conversation id, folder and scope)";
|
|
2264
|
+
|
|
2215
2265
|
/** How often, and how long, "after this turn" checks its turn (AG-18). */
|
|
2216
2266
|
export const TURN_POLL_MS = 1_000;
|
|
2217
2267
|
export const TURN_WAIT_MS = 30 * 60_000;
|
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
type AgentsDeps,
|
|
96
96
|
type AgentsResolved,
|
|
97
97
|
type EarlyExit,
|
|
98
|
+
FRESH_NOTE,
|
|
98
99
|
type RuntimeChoice,
|
|
99
100
|
type Started,
|
|
100
101
|
} from "./agent-ops.js";
|
|
@@ -420,7 +421,9 @@ async function persona(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
420
421
|
return report(
|
|
421
422
|
d,
|
|
422
423
|
s,
|
|
423
|
-
|
|
424
|
+
s.fresh
|
|
425
|
+
? `agent ${s.rec.id} is now ${s.rec.persona} (was ${s.from}); ${FRESH_NOTE}, as ${s.rec.sessionName} in ${s.rec.cwd}`
|
|
426
|
+
: `agent ${s.rec.id} is now ${s.rec.persona} (was ${s.from}); same conversation, as ${s.rec.sessionName} in ${s.rec.cwd}`,
|
|
424
427
|
);
|
|
425
428
|
}
|
|
426
429
|
|
|
@@ -468,10 +471,7 @@ async function resume(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
468
471
|
if (args.includes("--all-interrupted")) return resumeAll(args, d);
|
|
469
472
|
const { id, ws } = idAndWorkspace("agents resume", args);
|
|
470
473
|
const s = await resumeAgent(d, id, ws);
|
|
471
|
-
if (s.fresh)
|
|
472
|
-
d.out(
|
|
473
|
-
`agent ${s.rec.id}: nothing to resume yet; started fresh (same conversation id, folder and scope)`,
|
|
474
|
-
);
|
|
474
|
+
if (s.fresh) d.out(`agent ${s.rec.id}: ${FRESH_NOTE}`);
|
|
475
475
|
return report(
|
|
476
476
|
d,
|
|
477
477
|
s,
|
|
@@ -140,6 +140,7 @@ import {
|
|
|
140
140
|
type AgentsDeps,
|
|
141
141
|
type AgentsResolved,
|
|
142
142
|
type Started,
|
|
143
|
+
FRESH_NOTE,
|
|
143
144
|
} from "../cli/agent-ops.js";
|
|
144
145
|
import { agentSessionName } from "../../shared/agents/session-name.js";
|
|
145
146
|
import { recordAction } from "./audit.js";
|
|
@@ -314,6 +315,8 @@ interface StartBody {
|
|
|
314
315
|
instruction?: unknown;
|
|
315
316
|
/** AG-10.10: a display name, checked as a rename's is. */
|
|
316
317
|
name?: unknown;
|
|
318
|
+
/** 0.4.18: the folder it runs in (Start agent here). */
|
|
319
|
+
folder?: string;
|
|
317
320
|
}
|
|
318
321
|
|
|
319
322
|
const START_KEYS = [
|
|
@@ -326,6 +329,7 @@ const START_KEYS = [
|
|
|
326
329
|
"allow",
|
|
327
330
|
"instruction",
|
|
328
331
|
"name",
|
|
332
|
+
"folder",
|
|
329
333
|
];
|
|
330
334
|
|
|
331
335
|
async function startBody(req: http.IncomingMessage): Promise<StartBody> {
|
|
@@ -373,6 +377,11 @@ async function startBody(req: http.IncomingMessage): Promise<StartBody> {
|
|
|
373
377
|
if (o.instruction !== undefined) out.instruction = o.instruction;
|
|
374
378
|
if (o.name !== undefined && o.name !== null && o.name !== "")
|
|
375
379
|
out.name = o.name;
|
|
380
|
+
if (o.folder !== undefined && o.folder !== null && o.folder !== "") {
|
|
381
|
+
if (typeof o.folder !== "string" || o.folder.length > 4096)
|
|
382
|
+
throw new AgentError("folder must be a path", { usage: true });
|
|
383
|
+
out.folder = o.folder;
|
|
384
|
+
}
|
|
376
385
|
return out as StartBody;
|
|
377
386
|
}
|
|
378
387
|
|
|
@@ -584,6 +593,7 @@ export async function handleAgentRoutes(
|
|
|
584
593
|
: {}),
|
|
585
594
|
...(body.allow !== undefined ? { allow: body.allow } : {}),
|
|
586
595
|
...(body.name !== undefined ? { name: body.name } : {}),
|
|
596
|
+
...(body.folder !== undefined ? { folder: body.folder } : {}),
|
|
587
597
|
strict: true,
|
|
588
598
|
});
|
|
589
599
|
const ov = s.rec.overrides?.length
|
|
@@ -714,7 +724,9 @@ export async function handleAgentRoutes(
|
|
|
714
724
|
}
|
|
715
725
|
const s = await change();
|
|
716
726
|
audit(what, s.early ? "exited at once" : undefined);
|
|
717
|
-
|
|
727
|
+
// Never prompted: nothing was resumed, so it says so (not "same
|
|
728
|
+
// conversation").
|
|
729
|
+
sendStarted(res, s, d, s.fresh ? { note: FRESH_NOTE } : {});
|
|
718
730
|
} else if (verb === "remove") {
|
|
719
731
|
const removed = await removeAgent(d, id!, ws);
|
|
720
732
|
audit(what);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How health checks run (0.4.17): at most CHECK_CONCURRENCY at once, each
|
|
3
|
+
* given at most CHECK_TIMEOUT_MS. A check that runs over its time gets the
|
|
4
|
+
* caller's stand-in result ("timed out after 10 s") rather than holding up
|
|
5
|
+
* the run, and the run never waits on it again.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The most checks in flight at once. */
|
|
9
|
+
export const CHECK_CONCURRENCY = 12;
|
|
10
|
+
/** The longest one check may take, whatever its kind. */
|
|
11
|
+
export const CHECK_TIMEOUT_MS = 10_000;
|
|
12
|
+
|
|
13
|
+
/** What a check that ran out of time says. */
|
|
14
|
+
export function timedOutText(ms: number): string {
|
|
15
|
+
return `timed out after ${Math.round(ms / 1000)} s`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `p`, or `late()` once `ms` passes first. A late `p` that later settles
|
|
20
|
+
* is ignored, and so is its rejection.
|
|
21
|
+
*/
|
|
22
|
+
export function withTimeout<T>(
|
|
23
|
+
p: Promise<T>,
|
|
24
|
+
ms: number,
|
|
25
|
+
late: () => T,
|
|
26
|
+
): Promise<T> {
|
|
27
|
+
return new Promise<T>((resolve, reject) => {
|
|
28
|
+
let done = false;
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
if (done) return;
|
|
31
|
+
done = true;
|
|
32
|
+
resolve(late());
|
|
33
|
+
}, ms);
|
|
34
|
+
timer.unref?.();
|
|
35
|
+
p.then(
|
|
36
|
+
(v) => {
|
|
37
|
+
if (done) return;
|
|
38
|
+
done = true;
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
resolve(v);
|
|
41
|
+
},
|
|
42
|
+
(e: unknown) => {
|
|
43
|
+
if (done) return;
|
|
44
|
+
done = true;
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
reject(e);
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `fn` over `items`, at most `limit` at a time, the results in the items'
|
|
54
|
+
* order. Each starts as soon as a slot frees up.
|
|
55
|
+
*/
|
|
56
|
+
export async function runCapped<T, R>(
|
|
57
|
+
items: readonly T[],
|
|
58
|
+
limit: number,
|
|
59
|
+
fn: (item: T, index: number) => Promise<R>,
|
|
60
|
+
): Promise<R[]> {
|
|
61
|
+
const out = new Array<R>(items.length);
|
|
62
|
+
let next = 0;
|
|
63
|
+
const worker = async (): Promise<void> => {
|
|
64
|
+
while (next < items.length) {
|
|
65
|
+
const i = next++;
|
|
66
|
+
out[i] = await fn(items[i], i);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const n = Math.max(1, Math.min(limit, items.length));
|
|
70
|
+
await Promise.all(Array.from({ length: n }, worker));
|
|
71
|
+
return out;
|
|
72
|
+
}
|