@rubytech/create-maxy-code 0.1.92 → 0.1.93
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/dist/__tests__/claude-ptys-slice.test.js +25 -0
- package/dist/index.js +10 -1
- package/dist/port-resolution.js +29 -0
- package/package.json +1 -1
- package/payload/platform/plugins/docs/references/platform.md +4 -2
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +61 -2
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +337 -32
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +1 -3
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +307 -209
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +62 -13
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts +9 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.js +14 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +165 -33
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +283 -92
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts +4 -2
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.js +23 -22
- package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +55 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +170 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +114 -0
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +209 -0
- package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -0
- package/payload/server/public/assets/{ChatInput-dLWJ0Q8I.css → ChatInput-CzQhPt90.css} +1 -1
- package/payload/server/public/assets/{Checkbox-Bx8mRHQ3.js → Checkbox-ynZtDWi3.js} +1 -1
- package/payload/server/public/assets/{admin-BhEvxMrC.js → admin-ux6TKHS1.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BEW_bD50.js → architectureDiagram-Q4EWVU46-Ko_izKmY.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DFLT98C3.js → blockDiagram-DXYQGD6D-D58VDC0w.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-BemKtROA.js → c4Diagram-AHTNJAMY-C-eRXyKi.js} +1 -1
- package/payload/server/public/assets/channel-CejaITMF.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-BL8fGJ7_.js → chunk-336JU56O-DZLEfLAb.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-BD6by0MP.js → chunk-426QAEUC-BriTU3L8.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-DX-cb6Nd.js → chunk-4TB4RGXK-BdxRL-AY.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-B2QqtIZS.js → chunk-5FUZZQ4R-BgEmy7Y3.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-CzJWmsmd.js → chunk-5PVQY5BW-emnbDRLA.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-DP9vcvEU.js → chunk-EDXVE4YY-BvCf2Xf2.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-D5xWEjF0.js → chunk-ENJZ2VHE-DTpEYJRb.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-AKduf9o4.js → chunk-ICPOFSXX-Ced2j48Y.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-D6jsVNyQ.js → chunk-OYMX7WX6-B21_a3Dv.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-BNAnTYh8.js → chunk-U2HBQHQK-CPS3yyXK.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-UdQhYDnh.js → chunk-X2U36JSP-BJ_B5NW7.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-DSSmSGmS.js → chunk-YZCP3GAM-CMLy-WXm.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-Czm7IDMC.js → chunk-ZZ45TVLE-BkLnzCHe.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BmoqALf4.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-vMajwCNi.js +1 -0
- package/payload/server/public/assets/clone-mvVfHkyg.js +1 -0
- package/payload/server/public/assets/{dagre-hSIMHJcO.js → dagre-Dd_gq4yj.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-CBuRCJE7.js → dagre-KV5264BT-RY0E5UAn.js} +1 -1
- package/payload/server/public/assets/data-BK4r8Dqy.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-dbMrH0le.js → diagram-5BDNPKRD-DR7XiPLh.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-DBc6hs9a.js → diagram-G4DWMVQ6-2EbOfKFs.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-ubSGybUk.js → diagram-MMDJMWI5-CGcY0Ze7.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-WxL3cv-9.js → diagram-TYMM5635-Dk6MmAso.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-CrBBOAN8.js → erDiagram-SMLLAGMA-CKvdlUqk.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CXUq5Hzt.js → flowDiagram-DWJPFMVM-BqKhvCDz.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-vTCBnYVS.js → ganttDiagram-T4ZO3ILL-CeiE-IRb.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BHrj_bRa.js → gitGraphDiagram-UUTBAWPF-BbPKn7v2.js} +1 -1
- package/payload/server/public/assets/graph-Cb6A8Hhs.js +1 -0
- package/payload/server/public/assets/graph-labels-DCsUNtnE.js +1 -0
- package/payload/server/public/assets/{graphlib-CDO75l2X.js → graphlib-tG6kR59B.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CcQdaOhi.js → infoDiagram-42DDH7IO-6MiRGCI1.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-3KvltFgV.js → ishikawaDiagram-UXIWVN3A-BwY4p5mg.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DZCXaGFE.js → journeyDiagram-VCZTEJTY-BXAQ_fUi.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DAIAKy1-.js → kanban-definition-6JOO6SKY-ChiiO7Yt.js} +1 -1
- package/payload/server/public/assets/{lib-CCLURO3E.js → lib-DbPz9cfu.js} +1 -1
- package/payload/server/public/assets/{line-C8iCE2Wx.js → line-B5g5OrOD.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-ElvUhnV9.js → mermaid-parser.core-BMGzMKJx.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-7dSH31Ao.js → mermaid.core-DLMgkjXn.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-FZ0rcfac.js → mindmap-definition-QFDTVHPH-BvyzRLAi.js} +1 -1
- package/payload/server/public/assets/{page-C1hj89Gf.js → page-BeYR_GI2.js} +1 -1
- package/payload/server/public/assets/{page-Bu8kO4jO.js → page-DVNp-Dfy.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CDuvMZbO.js → pieDiagram-DEJITSTG--EV9pVfs.js} +1 -1
- package/payload/server/public/assets/{public-g_2skbnY.js → public-sYW0Dwku.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-xDjhkqNc.js → quadrantDiagram-34T5L4WZ-DJcL08ft.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-BPKpyT0n.js → requirementDiagram-MS252O5E-D4VdmmSu.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BvFIrihC.js → sankeyDiagram-XADWPNL6-Dsw3nHRS.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-C6G1oAE0.js → sequenceDiagram-FGHM5R23-CAgemUx5.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DbOJT_Xh.js → stateDiagram-FHFEXIEX-B7dyL9_X.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CImN6Wk6.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-D1DSxHh6.js → timeline-definition-GMOUNBTQ-B7iKswhp.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BYoSeMdo.js → vennDiagram-DHZGUBPP-uFS0i1qX.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-DH3kZdea.js → wardleyDiagram-NUSXRM2D-lDUh4LAD.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-C6MROMLG.js → xychartDiagram-5P7HB3ND-0v7MEVwW.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/public/assets/channel-CfpByn-m.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DNc9R1IX.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DM1DQh8N.js +0 -1
- package/payload/server/public/assets/clone-BSrNdvLW.js +0 -1
- package/payload/server/public/assets/data-Cbo0tTS3.js +0 -1
- package/payload/server/public/assets/graph-Beucu3_J.js +0 -1
- package/payload/server/public/assets/graph-labels-D15-eFQK.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ce4GFk-H.js +0 -1
- /package/payload/server/public/assets/{ChatInput-DoS80kaC.js → ChatInput-Cj9JQtYt.js} +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Task 250 — `claude-ptys.slice` unit emitted at install time. Every
|
|
2
|
+
// `claude-session-*.scope` the session manager spawns drops under this
|
|
3
|
+
// slice, so the operator can see the cohort with one `systemctl --user
|
|
4
|
+
// status claude-ptys.slice`. This test pins the unit file shape so a
|
|
5
|
+
// future refactor cannot silently drop [Install] (which would leave the
|
|
6
|
+
// slice un-enabled after install).
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { buildClaudePtysSliceUnitFile } from "../port-resolution.js";
|
|
10
|
+
test("buildClaudePtysSliceUnitFile emits a valid systemd slice unit", () => {
|
|
11
|
+
const unit = buildClaudePtysSliceUnitFile();
|
|
12
|
+
assert.match(unit, /^\[Unit\]$/m);
|
|
13
|
+
assert.match(unit, /^Description=Claude Code PTY sessions \(Task 250\)$/m);
|
|
14
|
+
assert.match(unit, /^\[Slice\]$/m);
|
|
15
|
+
assert.match(unit, /^\[Install\]$/m);
|
|
16
|
+
assert.match(unit, /^WantedBy=default\.target$/m);
|
|
17
|
+
});
|
|
18
|
+
test("buildClaudePtysSliceUnitFile output has no per-brand or per-install template tokens", () => {
|
|
19
|
+
// The slice is brand-agnostic. If a refactor accidentally introduces a
|
|
20
|
+
// template token (e.g. ${INSTALL_DIR}), the slice file would be invalid
|
|
21
|
+
// after install. Catch the regression at the source.
|
|
22
|
+
const unit = buildClaudePtysSliceUnitFile();
|
|
23
|
+
assert.doesNotMatch(unit, /\$\{/);
|
|
24
|
+
assert.doesNotMatch(unit, /__[A-Z_]+__/);
|
|
25
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, rea
|
|
|
4
4
|
import { registerSpecialistAgentsAt, SpecialistSymlinkCollision } from "./specialist-registration.js";
|
|
5
5
|
import { resolve, join, dirname } from "node:path";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
|
-
import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile } from "./port-resolution.js";
|
|
7
|
+
import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile, buildClaudePtysSliceUnitFile } from "./port-resolution.js";
|
|
8
8
|
import { validateTierFlag, validateEntitlementBase64, applyTierToAccountConfig, entitlementPath } from "./tier-flag.js";
|
|
9
9
|
import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
|
|
10
10
|
import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
|
|
@@ -3043,6 +3043,15 @@ function installService() {
|
|
|
3043
3043
|
});
|
|
3044
3044
|
writeFileSync(join(serviceDir, claudeSessionManagerUnitName), claudeSessionManagerUnit);
|
|
3045
3045
|
logFile(` ${claudeSessionManagerUnitName}: CLAUDE_SESSION_MANAGER_PORT=${CLAUDE_SESSION_MANAGER_PORT_BRAND} MAXY_UI_INTERNAL_PORT=${MAXY_UI_INTERNAL_PORT}`);
|
|
3046
|
+
// Task 250 — write the brand-agnostic `claude-ptys.slice` unit. Every
|
|
3047
|
+
// brand's session manager drops its per-spawn scope units under this
|
|
3048
|
+
// slice. Writing it once per install is idempotent (the content is
|
|
3049
|
+
// identical across brands and across runs); subsequent brand installs
|
|
3050
|
+
// re-stamp the same content. No service restart is needed — `daemon-reload`
|
|
3051
|
+
// below picks it up.
|
|
3052
|
+
const claudePtysSliceName = "claude-ptys.slice";
|
|
3053
|
+
writeFileSync(join(serviceDir, claudePtysSliceName), buildClaudePtysSliceUnitFile());
|
|
3054
|
+
logFile(` ${claudePtysSliceName}: Task 250 slice for claude-session-*.scope cohort`);
|
|
3046
3055
|
// the edge service: always-on front door that owns the public
|
|
3047
3056
|
// port (PORT) and the VNC stack (Xtigervnc + websockify). Its lifecycle is
|
|
3048
3057
|
// independent of the main brand service, so an in-place upgrade triggered
|
package/dist/port-resolution.js
CHANGED
|
@@ -156,3 +156,32 @@ StandardError=append:${o.persistDir}/logs/server.log
|
|
|
156
156
|
WantedBy=default.target
|
|
157
157
|
`;
|
|
158
158
|
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Task 250 — `claude-ptys.slice` unit. Every claude PTY scope the session
|
|
161
|
+
// manager spawns (`claude-session-<token>.scope`) is created under this
|
|
162
|
+
// slice so the operator can see the cohort with a single `systemctl --user
|
|
163
|
+
// status claude-ptys.slice`. The slice is not strictly required for cgroup
|
|
164
|
+
// isolation — scopes ride directly under user.slice if no `--slice=` flag
|
|
165
|
+
// is given — but cohort observability and any future per-cohort resource
|
|
166
|
+
// limit (MemoryMax=, CPUWeight=) live on this unit.
|
|
167
|
+
//
|
|
168
|
+
// Brand-agnostic: there is one `claude-ptys.slice` shared across every
|
|
169
|
+
// brand on the host. The user-systemd manager owns the slice; multiple
|
|
170
|
+
// brands' session-managers can drop their per-spawn scopes under the same
|
|
171
|
+
// slice without interference because scopes are uniquely named via the
|
|
172
|
+
// per-spawn unit token. The installer writes the slice file at
|
|
173
|
+
// `~/.config/systemd/user/claude-ptys.slice` and runs
|
|
174
|
+
// `systemctl --user daemon-reload` so the new unit is picked up.
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
export function buildClaudePtysSliceUnitFile() {
|
|
177
|
+
return `[Unit]
|
|
178
|
+
Description=Claude Code PTY sessions (Task 250)
|
|
179
|
+
Documentation=https://github.com/rubytech/maxy-code/blob/main/.tasks/archive/250-claude-ptys-survive-manager-restart-via-systemd-scope-per-pty.md
|
|
180
|
+
Before=shutdown.target
|
|
181
|
+
|
|
182
|
+
[Slice]
|
|
183
|
+
|
|
184
|
+
[Install]
|
|
185
|
+
WantedBy=default.target
|
|
186
|
+
`;
|
|
187
|
+
}
|
package/package.json
CHANGED
|
@@ -82,7 +82,9 @@ The row feed sits behind `requireAdminSession` like every other admin route, so
|
|
|
82
82
|
|
|
83
83
|
The trade-off is a longer-lived connection per tab: the manager's per-process subscriber count rises with open tabs, and the SSE channel must survive proxy idle timeouts. The manager emits a 25-second keep-alive comment line on every connection (ignored by EventSource consumers, refreshes the proxy clock) and the browser-side store force-closes-and-reconnects on transport errors with exponential backoff capped at 30s.
|
|
84
84
|
|
|
85
|
-
The row payload carries `url: string | null` (
|
|
85
|
+
The row payload carries `url: string | null` (Tasks 189 / 260) — the `claude.ai/code/session_<suffix>` URL captured from the `/remote-control` banner. **Task 260 — disk is the only source of truth.** Spawn metadata that previously lived in an in-memory `SessionStore` (senderId, role, channel, url, startedAt, permissionMode, model, hidden, specialist) now rides a sidecar file alongside the JSONL: `<projectsDir>/<sessionId>.meta.json`. The watcher reads the sidecar at row-build time and stamps the nine fields onto the `SessionRow`; the serialiser reads `row.url` directly with no in-memory side channel. The value is `null` whenever the spawn is headless (`HEADLESS_ROLES`, Task 171 — `--remote-control` not passed), or before url-capture has fired on a channel-facing spawn (~2 s after spawn), or on rows whose JSONL+sidecar pair was archived before the banner landed. When url-capture eventually fires, `pty-spawner` writes the URL to the sidecar via `updateSidecar`, calls `watcher.refreshSidecar(sessionId)` to refresh the row index, and the manager pushes a `row-updated` SSE frame carrying the fresh URL — the client's Open-in-new-tab arrow appears in step. The Sidebar gates the arrow on `row.live && row.url !== null` and opens `row.url` directly with no `/meta` round-trip; each click logs `[admin-ui] sidebar-open-in-new-tab outcome=<ok|blocked> sessionId=<8-char>` (`blocked` fires when a popup blocker swallows `window.open`).
|
|
86
|
+
|
|
87
|
+
**Manager state shape (Task 260).** The manager keeps exactly two pieces of in-process state — the live `PtyHandle` map (in `pty-spawner.ts`, keyed on sessionId, holding the file descriptor and runtime flags that cannot go on disk) and the watcher's row index (rebuilt from disk on each event). Everything else lives on disk: the JSONL transcript at `<projectsDir>/<sessionId>.jsonl` (live) or `<projectsDir>/archive/<sessionId>.jsonl` (archived), the sidecar at the matching path with `.meta.json`, and the PID file at `${CLAUDE_CONFIG_DIR}/sessions/<pid>.json`. A manager restart re-reads the sidecars at boot — every row that had one before the restart appears in `/list` with full senderId/role/channel populated. Pre-Task-260 archived JSONLs (created before the sidecar writer existed) index normally but with seven null sidecar fields; `/list?senderId=...` hides them unless the caller passes `includeHidden=1`, and the boot-seed log line surfaces the count via `sidecars=<n>` vs `jsonl-total=<n>`. The watcher enumerates BOTH the top-level projects dir AND its `archive/` subdir, watches both with `fs.watch`, and coalesces a top↔archive rename into one `row-updated` event (no `row-removed` followed by `row-created` — the rename is one logical state change keyed on sessionId).
|
|
86
88
|
|
|
87
89
|
**Spawn lifecycle: PID-file driven.** Clicking "+ New session" opens the `NewSessionModal` (Task 223). Modal submit POSTs to the wrapper with the operator's typed text as `initialMessage`, plus per-session `permissionMode` and `model` overrides; only then does the PTY spawn. The manager waits for Claude Code's PID file at `${CLAUDE_CONFIG_DIR}/sessions/<pid>.json`. The PID file lands at process init (for `entrypoint: cli` spawns) and carries the intrinsic `sessionId`, `bridgeSessionId`, `agent`, and `status` directly. The manager's filesystem watcher reports the create event; the spawn response includes the canonical `sessionId` from that file. URL capture still runs in parallel to populate the operator-facing iframe URL, but it no longer gates readiness. The JSONL transcript is written on the first operator turn (true on 2.1.143 and 2.1.128); the watcher fires a separate event for that, and `/list`, `/meta`, `/log` resolve any of four ids — `sessionId`, `bridgeSessionId`, `bridgeSuffix`, or numeric `pid` — to the same row. The JSONL's first `role=user` line equals the operator's typed text byte-for-byte; Claude Code's `tail.aiTitle` is computed from that real content and remains the canonical sidebar row label. The wrapper at `platform/ui/server/routes/admin/claude-sessions.ts` is still the single canonical entry point for any programmatic admin spawn-with-prompt — see `admin-session.md` "Spawn-with-initialMessage wrapper" and `internals.md` "Programmatic spawn entry point" — and the turn-recorder loopback path forwards its own `initialMessage`. Resume flows are unaffected (the prior transcript is the stimulus).
|
|
88
90
|
|
|
@@ -96,7 +98,7 @@ Each session row also carries a small muted timestamp crumb under the name showi
|
|
|
96
98
|
|
|
97
99
|
**Download JSONL (Task 197).** `GET /<id>/log?download=1` is a one-shot byte-stream of the session's JSONL transcript with attachment-disposition headers, designed for the pane's **Download JSONL** button. Headers: `Content-Type: application/x-ndjson`, `Content-Disposition: attachment; filename="<sessionId>.jsonl"` (the basename is sanitised so any non-`[A-Za-z0-9._-]` character is replaced with underscore), `Cache-Control: no-store`. Four status branches: **200** with the byte-identical file body; **404** `{error: 'session-not-found'}` when the store has no row for the id; **202** `{pending: true, jsonlPath: null}` when the row exists but claude has not flushed the first turn yet; **404** `{error: 'jsonl-missing-on-disk'}` when the row carries a `jsonlPath` but the file has been removed under the manager (post-Purge race). The download branch is declared **before** the follow check, so `?download=1` always wins over `?follow=1` if both are set. The proxy at `app.get('/:sessionId/log')` rebuilds the upstream query from a fixed `follow|download` allowlist; inbound query keys outside that allowlist are dropped. Observability: `[claude-session-manager] log-download sessionId=<sid> bytes=<n> ms=<n>` lands per successful stream completion; the browser console emits `[admin-ui] pane-download-jsonl sessionId=<8> outcome=initiated` on click. `outcome=initiated` rather than `outcome=ok` is intentional — the handler resolves before the browser writes the bytes, so the log line names "the request was kicked off", not "the file landed". If the file does not appear in the operator's downloads folder, check the manager line for the bytes count and the browser's downloads UI for the suppression record. Auth is unchanged from the rest of the `/api/admin/claude-sessions` surface (cookie session via `requireAdminSession`); there is no new key surface.
|
|
98
100
|
|
|
99
|
-
**PTY lifecycle contract (Tasks 170 + 176).** A PTY reaches its end via one of two branches: **operator-request** (operator clicks End or the auto-archive Stop hook calls `
|
|
101
|
+
**PTY lifecycle contract (Tasks 170 + 176 + 260).** A PTY reaches its end via one of two branches: **operator-request** (operator clicks End or the auto-archive Stop hook calls `killSession`) or **natural-exit** (the claude child exits on its own — operator typed `/quit`, SIGINT in the PTY, crash, network drop on `--remote-control`). Both branches honour a single invariant: the pty master file descriptor is released by an explicit `pty.destroy()` and the in-process tracker entry is removed before the next `/list` or `/events` tick. As of Task 260 the tracker is a module-scoped `Map<sessionId, PtyTracker>` in `pty-spawner.ts` — the metadata-rich `SessionStore` is gone; the tracker holds only what the file system cannot (PtyHandle + pid + bridge ids + runtime flags). Without the explicit destroy, the master fd lingers in node-pty's internal socket until V8 GC finalises the IPty object — non-deterministic and accumulates under load until the kernel pty cap (Linux 3072, macOS 511) refuses new spawns. Without the explicit row removal, the manager shutdown loop SIGTERMs PIDs that already logged `process-exited`, masking the leak only because the manager restarts every few hours. When both branches fire on the same exit (operator clicks End and node-pty's `onExit` fans out the SIGTERM to both listeners), a per-row `fdReleased` flag short-circuits the second branch so `pty.destroy()` runs exactly once on the live socket — without the flag, the second call throws "socket already destroyed" and the operator-request line would falsely log `master-fd=close-failed`. If the first branch's destroy throws and is rescued, the flag stays unset and the second branch retries (defense in depth). Every `kill … pid=<n>` log line carries a `master-fd=closed` suffix (or `master-fd=close-failed err=<msg>` on the rescued throw branch — a graceful degradation so a corner-case socket-state failure cannot turn a logically-successful exit into a 500); the operator-request line additionally identifies `reason=operator-request`, the natural-exit line identifies `reason=process-exited`. Both branches are verified by the `stop-session-fd-release` and `endpoint-stop-delete` integration tests (operator-request live and already-exited cycles + natural-exit cycle + throw-then-retry coordination, Linux kernel-level ptmx fd accounting on each).
|
|
100
102
|
|
|
101
103
|
The metadata pane subscribes to the same /list projection. When an operator clicks End on an alive row, the DELETE returns 200 and the post-mutation refetch decides what happens next: a session that wrote a JSONL surfaces as a dehydrated `status: 'ended'` row (the pane swaps `End session` for `Purge JSONL` plus `Resume`), and a session that never wrote a JSONL (`Turns: 0`) leaves the list entirely (the pane shows a `Session ended without a transcript. Close this pane.` banner with a Close button and no destructive action). The manager's `/list` and `/meta` are the only authorities on post-End state; the client does not pre-empt either response with an optimistic mutation.
|
|
102
104
|
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import type { Logger } from './types.js';
|
|
1
|
+
import type { Logger, Role, Channel } from './types.js';
|
|
2
|
+
import { type SystemctlRunner } from './systemd-scope.js';
|
|
2
3
|
export type RowState = 'live' | 'archived';
|
|
3
|
-
|
|
4
|
+
/** Task 250 — `detached` is reached only via the boot-seed reconciler.
|
|
5
|
+
* When the manager restarts and finds a surviving `claude-session-*.scope`
|
|
6
|
+
* unit whose PID file is on disk, it reattaches the row but cannot read
|
|
7
|
+
* PTY bytes, inject keystrokes, or kill via the master fd (the manager no
|
|
8
|
+
* longer owns it). The detached row is observation-only on the manager
|
|
9
|
+
* side; the operator's claude.ai/code URL still works because Claude
|
|
10
|
+
* Code's remote-control channel is an outbound WebSocket independent of
|
|
11
|
+
* the local PTY. End-via-systemctl-stop still works because the scope
|
|
12
|
+
* unit name is recoverable. */
|
|
13
|
+
export type RowStatus = 'idle' | 'busy' | 'gone' | 'detached';
|
|
4
14
|
export interface SessionRow {
|
|
5
15
|
/** Intrinsic Claude session id (UUID). Canonical key. For a top-level
|
|
6
16
|
* jsonl row, this is the file basename. For a pre-first-turn live row,
|
|
@@ -26,6 +36,38 @@ export interface SessionRow {
|
|
|
26
36
|
status: RowStatus;
|
|
27
37
|
/** ms epoch from PID file `updatedAt`/`startedAt`, or 0 if unknown. */
|
|
28
38
|
updatedAt: number;
|
|
39
|
+
/** Task 250 — systemd scope unit token, set by boot-seed when the row
|
|
40
|
+
* matches a surviving `claude-session-*.scope`. Null for archived rows
|
|
41
|
+
* and for live rows whose owning manager process is the current
|
|
42
|
+
* process (those use PtyTracker.scopeUnitToken in pty-spawner's
|
|
43
|
+
* module-scoped livePtys map). When set on a SessionRow without a
|
|
44
|
+
* matching tracker, the row is a detached survivor whose End button
|
|
45
|
+
* should go through this unit token rather than the (missing)
|
|
46
|
+
* PtyHandle. */
|
|
47
|
+
scopeUnitToken: string | null;
|
|
48
|
+
/** From sidecar. Null when no sidecar on disk for this row. */
|
|
49
|
+
senderId: string | null;
|
|
50
|
+
/** From sidecar. Null when no sidecar on disk for this row. */
|
|
51
|
+
role: Role | null;
|
|
52
|
+
/** From sidecar. Null when no sidecar on disk for this row. */
|
|
53
|
+
channel: Channel | null;
|
|
54
|
+
/** From sidecar; updated by url-capture when the banner lands. Null
|
|
55
|
+
* for headless spawns and for rows whose live PTY exited before the
|
|
56
|
+
* banner fired. */
|
|
57
|
+
url: string | null;
|
|
58
|
+
/** From sidecar. ISO 8601 timestamp captured at /spawn. */
|
|
59
|
+
startedAt: string | null;
|
|
60
|
+
/** From sidecar. Null when not requested. */
|
|
61
|
+
permissionMode: string | null;
|
|
62
|
+
/** Task 260 — `hidden` from the sidecar. Defaults to `false` when no
|
|
63
|
+
* sidecar on disk. */
|
|
64
|
+
hidden: boolean;
|
|
65
|
+
/** From sidecar. Null when no sidecar on disk OR when the spawn was
|
|
66
|
+
* not a specialist. The `agent` field above is the in-process view
|
|
67
|
+
* (PID-file `--agent <name>`); this is the request as recorded at
|
|
68
|
+
* /spawn. They typically agree; divergence flags either a stale
|
|
69
|
+
* sidecar or a sidecar-less row. */
|
|
70
|
+
sidecarSpecialist: string | null;
|
|
29
71
|
}
|
|
30
72
|
export interface WatcherEvent {
|
|
31
73
|
kind: 'create' | 'modify' | 'delete';
|
|
@@ -45,6 +87,17 @@ export interface FsWatcher {
|
|
|
45
87
|
all(): SessionRow[];
|
|
46
88
|
/** Resolve once a PID-file event lands for `pid`. Rejects on `timeoutMs`. */
|
|
47
89
|
waitForPid(pid: number, timeoutMs: number): Promise<SessionRow>;
|
|
90
|
+
/** Task 260 — re-read the sidecar for `sessionId` and re-emit a
|
|
91
|
+
* `modify` event. fs.watch does not fire for `<sid>.meta.json` writes
|
|
92
|
+
* (the watcher only watches `*.jsonl` and PID files), so this is the
|
|
93
|
+
* trigger for the http-server's url-capture path: pty-spawner writes
|
|
94
|
+
* the URL to the sidecar, then calls this to refresh the row index
|
|
95
|
+
* and emit a row-updated SSE event.
|
|
96
|
+
*
|
|
97
|
+
* Returns true when the row exists (the refresh fired); false when
|
|
98
|
+
* no row matches the sessionId (caller may choose to log; the
|
|
99
|
+
* url-capture path early-returns on null). */
|
|
100
|
+
refreshSidecar(sessionId: string): boolean;
|
|
48
101
|
close(): void;
|
|
49
102
|
/** Test seam: drive reconciliation directly, bypassing fs.watch + debounce.
|
|
50
103
|
* Production code uses the same closures via the watch callback; the seam
|
|
@@ -60,5 +113,11 @@ export declare function createFsWatcher(opts: {
|
|
|
60
113
|
sessionsDir: string;
|
|
61
114
|
projectsDir: string;
|
|
62
115
|
logger: Logger;
|
|
116
|
+
/** Task 250 — systemctl runner the boot-seed uses to discover surviving
|
|
117
|
+
* `claude-session-*.scope` units. Default issues real spawnSync calls;
|
|
118
|
+
* tests inject a recording/scripted stub. When the env has no systemd
|
|
119
|
+
* user manager (macOS dev hosts), the default runner returns non-zero
|
|
120
|
+
* exit codes and the boot-seed degrades to "0 survivors" gracefully. */
|
|
121
|
+
runSystemctl?: SystemctlRunner;
|
|
63
122
|
}): FsWatcher;
|
|
64
123
|
//# sourceMappingURL=fs-watcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs-watcher.d.ts","sourceRoot":"","sources":["../src/fs-watcher.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"fs-watcher.d.ts","sourceRoot":"","sources":["../src/fs-watcher.ts"],"names":[],"mappings":"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"}
|