@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +94 -1
  4. package/package.json +1 -1
  5. package/src/ui/cli/agent-ops.ts +50 -0
  6. package/src/ui/cli/agents-command.ts +5 -5
  7. package/src/ui/server/agent-routes.ts +13 -1
  8. package/src/ui/server/check-pool.ts +72 -0
  9. package/src/ui/server/index.ts +105 -10
  10. package/src/ui/server/public/agents-page.js +98 -16
  11. package/src/ui/server/public/agents.html +22 -0
  12. package/src/ui/server/public/environments.html +223 -140
  13. package/src/ui/server/public/graph.css +40 -0
  14. package/src/ui/server/public/graph.js +47 -6
  15. package/src/ui/server/public/header.js +7 -0
  16. package/src/ui/server/public/i18n/es.js +46 -0
  17. package/src/ui/server/public/i18n/ja.js +46 -0
  18. package/src/ui/server/public/index.html +6 -0
  19. package/src/ui/server/public/local-dev.html +199 -27
  20. package/src/ui/server/public/log-source.js +26 -0
  21. package/src/ui/server/public/logs.html +7 -20
  22. package/src/ui/server/public/panel.js +1 -1
  23. package/src/ui/server/public/release.html +19 -1
  24. package/src/ui/server/public/services.html +18 -3
  25. package/src/ui/server/public/shell.js +360 -32
  26. package/src/ui/server/public/svc-panel.js +672 -0
  27. package/src/ui/server/public/topbar.js +34 -1
  28. package/src/ui/server/public/view-scope.js +61 -0
  29. package/src/ui/server/public/workspace-picker.js +2 -0
  30. package/src/ui/server/route-classes.ts +2 -0
  31. package/src/ui/server/run-history.ts +181 -0
  32. package/src/ui/server/session-routes.ts +94 -17
  33. 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.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">
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.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">
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.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">
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- `agent ${s.rec.id} is now ${s.rec.persona} (was ${s.from}); same conversation, as ${s.rec.sessionName} in ${s.rec.cwd}`,
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
- sendStarted(res, s, d);
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
+ }