@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
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
import { Hono } from 'hono';
|
|
25
25
|
import { stream } from 'hono/streaming';
|
|
26
26
|
import { existsSync, statSync, createReadStream, watchFile, unwatchFile, rmSync, mkdirSync, renameSync, readFileSync } from 'node:fs';
|
|
27
|
-
import { spawnClaudeSession, stopSession, PERMISSION_MODES } from './pty-spawner.js';
|
|
27
|
+
import { spawnClaudeSession, stopSession, writeInputToPty, isLive, onPtyExit, livePtyCount, getPtyTrackerForTests, PERMISSION_MODES, } from './pty-spawner.js';
|
|
28
|
+
import { readSidecar } from './session-sidecar.js';
|
|
28
29
|
import { readJsonlSession } from './jsonl-enumerator.js';
|
|
29
|
-
import { claudeStateRoot, projectSlugForCwd, jsonlPathForSessionId } from './jsonl-path.js';
|
|
30
|
+
import { claudeStateRoot, projectSlugForCwd, jsonlPathForSessionId, sidecarPathForSessionId } from './jsonl-path.js';
|
|
30
31
|
import { basename, join } from 'node:path';
|
|
31
32
|
import { validateUserTitle } from './user-title-store.js';
|
|
32
33
|
const ROLES = ['admin', 'public'];
|
|
@@ -166,11 +167,11 @@ function resolveRow(watcher, id) {
|
|
|
166
167
|
// auto-archive` lines cover the lifecycle.
|
|
167
168
|
const END_TURN_SIGNATURE = '"stop_reason":"end_turn"';
|
|
168
169
|
const AUTO_ARCHIVE_SPECIALIST = 'database-operator';
|
|
169
|
-
function attachSpecialistEndTurnAutoArchive(deps,
|
|
170
|
-
if (
|
|
170
|
+
function attachSpecialistEndTurnAutoArchive(deps, sessionId, specialist) {
|
|
171
|
+
if (specialist !== AUTO_ARCHIVE_SPECIALIST)
|
|
171
172
|
return;
|
|
172
173
|
let archived = false;
|
|
173
|
-
const jsonlPath = jsonlPathForSessionId(deps.spawnCwd,
|
|
174
|
+
const jsonlPath = jsonlPathForSessionId(deps.spawnCwd, sessionId);
|
|
174
175
|
function endTurnSeen() {
|
|
175
176
|
try {
|
|
176
177
|
const content = readFileSync(jsonlPath, 'utf8');
|
|
@@ -188,35 +189,38 @@ function attachSpecialistEndTurnAutoArchive(deps, session) {
|
|
|
188
189
|
if (!endTurnSeen())
|
|
189
190
|
return;
|
|
190
191
|
archived = true;
|
|
191
|
-
deps.logger(`auto-archive sessionId=${
|
|
192
|
-
void stopSession({
|
|
192
|
+
deps.logger(`auto-archive sessionId=${sessionId} specialist=${AUTO_ARCHIVE_SPECIALIST} reason=end-turn archivedAt=${new Date().toISOString()}`);
|
|
193
|
+
void stopSession({
|
|
194
|
+
killGraceMs: deps.killGraceMs,
|
|
195
|
+
logger: deps.logger,
|
|
196
|
+
runSystemctl: deps.runSystemctl,
|
|
197
|
+
}, sessionId);
|
|
193
198
|
}
|
|
194
199
|
tryArchive();
|
|
195
200
|
const unsubscribe = deps.watcher.subscribe((event) => {
|
|
196
201
|
if (archived)
|
|
197
202
|
return;
|
|
198
|
-
if (event.sessionId !==
|
|
203
|
+
if (event.sessionId !== sessionId)
|
|
199
204
|
return;
|
|
200
205
|
if (event.source !== 'jsonl')
|
|
201
206
|
return;
|
|
202
207
|
tryArchive();
|
|
203
208
|
});
|
|
204
|
-
|
|
209
|
+
onPtyExit(sessionId, () => unsubscribe());
|
|
205
210
|
}
|
|
206
|
-
function attachPublicAudit(deps,
|
|
207
|
-
if (
|
|
211
|
+
function attachPublicAudit(deps, sessionId, role) {
|
|
212
|
+
if (role !== 'public')
|
|
208
213
|
return;
|
|
209
|
-
const
|
|
210
|
-
const jsonlPath = jsonlPathForSessionId(deps.spawnCwd, claudeSessionId);
|
|
214
|
+
const jsonlPath = jsonlPathForSessionId(deps.spawnCwd, sessionId);
|
|
211
215
|
if (!existsSync(jsonlPath))
|
|
212
216
|
return;
|
|
213
|
-
deps.publicToolAudit.attach(
|
|
214
|
-
if (
|
|
215
|
-
deps.publicToolAudit.detach(
|
|
217
|
+
deps.publicToolAudit.attach(sessionId, jsonlPath);
|
|
218
|
+
if (!isLive(sessionId)) {
|
|
219
|
+
deps.publicToolAudit.detach(sessionId);
|
|
216
220
|
return;
|
|
217
221
|
}
|
|
218
|
-
|
|
219
|
-
deps.publicToolAudit.detach(
|
|
222
|
+
onPtyExit(sessionId, () => {
|
|
223
|
+
deps.publicToolAudit.detach(sessionId);
|
|
220
224
|
});
|
|
221
225
|
}
|
|
222
226
|
export function buildHttpApp(deps) {
|
|
@@ -245,8 +249,10 @@ export function buildHttpApp(deps) {
|
|
|
245
249
|
}
|
|
246
250
|
app.__notifyRenameForTests = notifyRename;
|
|
247
251
|
// Watcher subscriber: when the JSONL CREATE event fires for a live
|
|
248
|
-
// session, attach the public-audit watcher. The
|
|
249
|
-
// (Task
|
|
252
|
+
// session, attach the public-audit watcher. The watcher row carries
|
|
253
|
+
// the role from the sidecar (Task 260 — disk is the source of truth),
|
|
254
|
+
// so the audit attachment fires deterministically against rows whose
|
|
255
|
+
// PTY is still alive in this manager process.
|
|
250
256
|
deps.watcher.subscribe((event) => {
|
|
251
257
|
if (event.source !== 'jsonl')
|
|
252
258
|
return;
|
|
@@ -254,10 +260,9 @@ export function buildHttpApp(deps) {
|
|
|
254
260
|
return;
|
|
255
261
|
if (!event.row)
|
|
256
262
|
return;
|
|
257
|
-
|
|
258
|
-
if (!live)
|
|
263
|
+
if (!isLive(event.sessionId))
|
|
259
264
|
return;
|
|
260
|
-
attachPublicAudit(deps,
|
|
265
|
+
attachPublicAudit(deps, event.sessionId, event.row.role);
|
|
261
266
|
});
|
|
262
267
|
// Task 183 — dispatch jsonl modify/create events into the recorder
|
|
263
268
|
// tail. The tail filters by registered sessionId (only specialist
|
|
@@ -279,75 +284,71 @@ export function buildHttpApp(deps) {
|
|
|
279
284
|
tail.onJsonlModify(event.sessionId, path);
|
|
280
285
|
});
|
|
281
286
|
}
|
|
282
|
-
/** Task
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const enriched = readJsonlSession(deps.spawnCwd, s.sessionId, LIST_TAIL_BYTES, deps.logger, deps.userTitleStore);
|
|
293
|
-
if (enriched)
|
|
294
|
-
return { displayName: enriched.displayName, titleSource: enriched.titleSource };
|
|
287
|
+
/** Task 260 — /spawn and /resume need to return a wire payload
|
|
288
|
+
* immediately, but at that moment the watcher row may not yet
|
|
289
|
+
* include the sidecar fields (the JSONL CREATE event hasn't fired
|
|
290
|
+
* in fresh-spawn tests with synthetic watchers). Build the payload
|
|
291
|
+
* from the row if present, falling back to a synthetic shape that
|
|
292
|
+
* reads the sidecar directly. */
|
|
293
|
+
function buildSpawnReturnPayload(sessionId, pid, startedAt, capturedUrl) {
|
|
294
|
+
const row = deps.watcher.getBySessionId(sessionId);
|
|
295
|
+
if (row && row.senderId !== null) {
|
|
296
|
+
return toPayload(row);
|
|
295
297
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
/** Task 258/259 — derived: a session is "archived" when its canonical
|
|
302
|
-
* top-level jsonl is absent but the archive/ variant exists. After the
|
|
303
|
-
* Task 258 refactor the watcher's row index doesn't surface archive/
|
|
304
|
-
* rows; the field stays on the wire so the Sidebar/SessionMetadataPane
|
|
305
|
-
* Unarchive affordance remains type-stable. A future task that
|
|
306
|
-
* enumerates archive/ jsonls into the row index will populate this
|
|
307
|
-
* honestly for archived rows. */
|
|
308
|
-
function isArchivedJsonlPath(sessionId) {
|
|
309
|
-
const canonical = jsonlPathForSessionId(deps.spawnCwd, sessionId);
|
|
310
|
-
if (existsSync(canonical))
|
|
311
|
-
return false;
|
|
312
|
-
return existsSync(jsonlPathForSessionId(deps.spawnCwd, sessionId, { archived: true }));
|
|
313
|
-
}
|
|
314
|
-
function toLivePayload(s) {
|
|
315
|
-
const jsonlPath = jsonlPathForSessionId(deps.spawnCwd, s.sessionId);
|
|
298
|
+
// Read the sidecar directly — the spawn just wrote it.
|
|
299
|
+
const sidecarPath = sidecarPathForSessionId(deps.spawnCwd, sessionId);
|
|
300
|
+
const { sidecar } = readSidecar(sidecarPath, deps.logger);
|
|
301
|
+
const jsonlPath = jsonlPathForSessionId(deps.spawnCwd, sessionId);
|
|
316
302
|
const hasJsonl = existsSync(jsonlPath);
|
|
317
|
-
const title = liveTitleAndSource(s);
|
|
318
303
|
return {
|
|
319
|
-
sessionId
|
|
320
|
-
pid
|
|
321
|
-
url:
|
|
322
|
-
startedAt:
|
|
323
|
-
senderId:
|
|
324
|
-
role:
|
|
325
|
-
channel:
|
|
304
|
+
sessionId,
|
|
305
|
+
pid,
|
|
306
|
+
url: capturedUrl ?? sidecar?.url ?? null,
|
|
307
|
+
startedAt: sidecar?.startedAt ?? startedAt,
|
|
308
|
+
senderId: sidecar?.senderId ?? '',
|
|
309
|
+
role: sidecar?.role ?? null,
|
|
310
|
+
channel: sidecar?.channel ?? null,
|
|
326
311
|
jsonlPath: hasJsonl ? jsonlPath : null,
|
|
327
312
|
status: 'alive',
|
|
328
313
|
archived: false,
|
|
329
|
-
displayName:
|
|
330
|
-
titleSource:
|
|
314
|
+
displayName: null,
|
|
315
|
+
titleSource: null,
|
|
331
316
|
lastMessageAt: null,
|
|
332
317
|
turnCount: 0,
|
|
333
318
|
capped: false,
|
|
334
319
|
sizeBytes: hasJsonl ? statSync(jsonlPath).size : null,
|
|
335
|
-
hidden:
|
|
336
|
-
specialist:
|
|
320
|
+
hidden: sidecar?.hidden ?? false,
|
|
321
|
+
specialist: sidecar?.specialist ?? null,
|
|
337
322
|
model: null,
|
|
338
323
|
};
|
|
339
324
|
}
|
|
340
|
-
|
|
325
|
+
/** Task 260 — single payload builder. Both live and archived rows
|
|
326
|
+
* derive every wire field from the watcher row (which itself derived
|
|
327
|
+
* from the sidecar + JSONL + PID file). The split between
|
|
328
|
+
* `toLivePayload`/`toArchivedPayload` is gone; the row's `state`
|
|
329
|
+
* and `pid` decide `status` and the `archived` flag.
|
|
330
|
+
*
|
|
331
|
+
* The `model` field continues to derive from the JSONL tail (last
|
|
332
|
+
* assistant turn) rather than the sidecar's spawn-request model —
|
|
333
|
+
* see [[learnings_writer_reader_charset_drift]] family: the running
|
|
334
|
+
* model is what the wire's `model` has historically meant, and the
|
|
335
|
+
* sidecar's `model` is kept as a self-diagnostic record. */
|
|
336
|
+
function toPayload(row) {
|
|
337
|
+
const isLiveState = row.state === 'live';
|
|
338
|
+
const archivedJsonlPath = jsonlPathForSessionId(deps.spawnCwd, row.sessionId, { archived: true });
|
|
339
|
+
const topJsonlPath = jsonlPathForSessionId(deps.spawnCwd, row.sessionId);
|
|
340
|
+
const jsonlPath = isLiveState
|
|
341
|
+
? topJsonlPath
|
|
342
|
+
: existsSync(archivedJsonlPath) ? archivedJsonlPath : topJsonlPath;
|
|
343
|
+
const hasJsonl = existsSync(jsonlPath);
|
|
341
344
|
let displayName = null;
|
|
342
345
|
let titleSource = null;
|
|
343
346
|
let lastMessageAt = null;
|
|
344
347
|
let turnCount = 0;
|
|
345
348
|
let capped = false;
|
|
346
|
-
let sizeBytes = null;
|
|
349
|
+
let sizeBytes = hasJsonl ? statSync(jsonlPath).size : null;
|
|
347
350
|
let model = null;
|
|
348
|
-
let
|
|
349
|
-
const jsonlPath = jsonlPathForSessionId(deps.spawnCwd, row.sessionId);
|
|
350
|
-
const hasJsonl = existsSync(jsonlPath);
|
|
351
|
+
let enrichedStartedAt = null;
|
|
351
352
|
if (hasJsonl) {
|
|
352
353
|
const enriched = readJsonlSession(deps.spawnCwd, row.sessionId, LIST_TAIL_BYTES, deps.logger, deps.userTitleStore);
|
|
353
354
|
if (enriched) {
|
|
@@ -357,38 +358,49 @@ export function buildHttpApp(deps) {
|
|
|
357
358
|
turnCount = enriched.turnCount;
|
|
358
359
|
capped = enriched.capped;
|
|
359
360
|
sizeBytes = enriched.sizeBytes;
|
|
360
|
-
|
|
361
|
+
enrichedStartedAt = enriched.startedAt;
|
|
361
362
|
model = enriched.model;
|
|
362
363
|
}
|
|
363
364
|
}
|
|
364
365
|
else {
|
|
365
|
-
// JSONL gone but user override may still exist (operator renamed the row
|
|
366
|
-
// before the cascade deleted the file). Surface the user title.
|
|
367
366
|
const userTitle = deps.userTitleStore.get(row.sessionId)?.title;
|
|
368
367
|
if (userTitle) {
|
|
369
368
|
displayName = userTitle;
|
|
370
369
|
titleSource = 'user';
|
|
371
370
|
}
|
|
372
371
|
}
|
|
372
|
+
// startedAt resolution order:
|
|
373
|
+
// 1. JSONL enrichment (the most authoritative — claude's own record)
|
|
374
|
+
// 2. Sidecar (/spawn time)
|
|
375
|
+
// 3. Row mtime fallback (legacy archived rows without sidecar)
|
|
376
|
+
const startedAt = enrichedStartedAt
|
|
377
|
+
?? row.startedAt
|
|
378
|
+
?? new Date(row.updatedAt || 0).toISOString();
|
|
379
|
+
// Agent label: PID-file `agent` for live rows; cachedAgentSetting for
|
|
380
|
+
// archived rows (JSONL head). Sidecar's `sidecarSpecialist` is the
|
|
381
|
+
// spawn-time request — fall back to it when both above are null so
|
|
382
|
+
// post-Task-260 rows surface their specialist even if the JSONL
|
|
383
|
+
// doesn't include an agent-setting record.
|
|
384
|
+
const specialist = row.agent ?? row.cachedAgentSetting ?? row.sidecarSpecialist;
|
|
373
385
|
return {
|
|
374
386
|
sessionId: row.sessionId,
|
|
375
|
-
pid:
|
|
376
|
-
url:
|
|
387
|
+
pid: row.pid,
|
|
388
|
+
url: row.url,
|
|
377
389
|
startedAt,
|
|
378
|
-
senderId,
|
|
379
|
-
role:
|
|
380
|
-
channel:
|
|
390
|
+
senderId: row.senderId ?? '',
|
|
391
|
+
role: row.role,
|
|
392
|
+
channel: row.channel,
|
|
381
393
|
jsonlPath: hasJsonl ? jsonlPath : null,
|
|
382
|
-
status: 'ended',
|
|
383
|
-
archived:
|
|
394
|
+
status: isLiveState ? 'alive' : 'ended',
|
|
395
|
+
archived: row.state === 'archived',
|
|
384
396
|
displayName,
|
|
385
397
|
titleSource,
|
|
386
398
|
lastMessageAt,
|
|
387
399
|
turnCount,
|
|
388
400
|
capped,
|
|
389
401
|
sizeBytes,
|
|
390
|
-
hidden:
|
|
391
|
-
specialist
|
|
402
|
+
hidden: row.hidden,
|
|
403
|
+
specialist,
|
|
392
404
|
model,
|
|
393
405
|
};
|
|
394
406
|
}
|
|
@@ -462,7 +474,16 @@ export function buildHttpApp(deps) {
|
|
|
462
474
|
: body.tunnelUrl === null
|
|
463
475
|
? null
|
|
464
476
|
: undefined;
|
|
465
|
-
const result = await spawnClaudeSession({
|
|
477
|
+
const result = await spawnClaudeSession({
|
|
478
|
+
...deps,
|
|
479
|
+
watcher: deps.watcher,
|
|
480
|
+
onSessionReady: (tracker) => {
|
|
481
|
+
attachPublicAudit(deps, tracker.sessionId, role);
|
|
482
|
+
attachSpecialistEndTurnAutoArchive(deps, tracker.sessionId, tracker.specialist);
|
|
483
|
+
},
|
|
484
|
+
onUrlCaptured: notifyUrlCaptured,
|
|
485
|
+
tunnelUrlOverride,
|
|
486
|
+
}, {
|
|
466
487
|
senderId,
|
|
467
488
|
userId,
|
|
468
489
|
role: role,
|
|
@@ -491,7 +512,8 @@ export function buildHttpApp(deps) {
|
|
|
491
512
|
return c.json({ error: 'spawn-failed', reason: result.reason, stderrTail: result.stderrTail }, 500);
|
|
492
513
|
}
|
|
493
514
|
timed(deps.logger, 'POST', '/spawn', 200, Date.now() - start);
|
|
494
|
-
|
|
515
|
+
const payload = buildSpawnReturnPayload(result.session.sessionId, result.session.pid, result.session.startedAt, result.session.url);
|
|
516
|
+
return c.json(payload, 200);
|
|
495
517
|
});
|
|
496
518
|
// SSE feed of row deltas. One channel per client; manager fans out from
|
|
497
519
|
// a single watcher subscription. On connect: replay current rows as
|
|
@@ -551,10 +573,17 @@ export function buildHttpApp(deps) {
|
|
|
551
573
|
firstPrompt,
|
|
552
574
|
titleSource,
|
|
553
575
|
updatedAt: row.updatedAt,
|
|
554
|
-
url
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
|
|
576
|
+
// Task 260 — url is on the row from the sidecar; url-capture
|
|
577
|
+
// mutates the sidecar in place and the watcher re-reads on the
|
|
578
|
+
// next event. Earlier this dipped into deps.store for the live-
|
|
579
|
+
// row case and returned null for everything else.
|
|
580
|
+
url: row.url,
|
|
581
|
+
// Task 260 — archive state is on the row directly (the watcher
|
|
582
|
+
// enumerates both top and archive subdirs and folds a top→archive
|
|
583
|
+
// move into a single state change). Earlier this was an extra
|
|
584
|
+
// existsSync against the archive path because archived rows
|
|
585
|
+
// weren't first-class members of the index.
|
|
586
|
+
archived: row.state === 'archived',
|
|
558
587
|
};
|
|
559
588
|
}
|
|
560
589
|
timed(deps.logger, 'GET', '/events', 200, Date.now() - start);
|
|
@@ -630,12 +659,13 @@ export function buildHttpApp(deps) {
|
|
|
630
659
|
deps.logger(`[admin-events] emit event=${sseEvent} sessionId=${event.sessionId} subscribers=1`);
|
|
631
660
|
void out.write(`event: ${sseEvent}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
632
661
|
});
|
|
633
|
-
// Task 189 — when `url-capture` fires for a session, the
|
|
634
|
-
// calls `notifyUrlCaptured(sessionId)` after
|
|
635
|
-
//
|
|
636
|
-
// Open-in-new-tab arrow appears in step. The row lookup goes
|
|
637
|
-
// the watcher (canonical row index); rowToPayload then
|
|
638
|
-
//
|
|
662
|
+
// Task 189 / 260 — when `url-capture` fires for a session, the
|
|
663
|
+
// pty-spawner calls `notifyUrlCaptured(sessionId)` after writing
|
|
664
|
+
// the URL to the sidecar. Re-emit the row so the client's hidden
|
|
665
|
+
// Open-in-new-tab arrow appears in step. The row lookup goes
|
|
666
|
+
// through the watcher (canonical row index); rowToPayload then
|
|
667
|
+
// reads `url` from the row (which the watcher just refreshed
|
|
668
|
+
// from the sidecar).
|
|
639
669
|
const urlListener = (sessionId) => {
|
|
640
670
|
if (!active)
|
|
641
671
|
return;
|
|
@@ -698,32 +728,41 @@ export function buildHttpApp(deps) {
|
|
|
698
728
|
timed(deps.logger, 'GET', '/list', 400, Date.now() - start);
|
|
699
729
|
return c.json({ error: 'senderId-required' }, 400);
|
|
700
730
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
//
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
731
|
+
// Task 260 — one source. Filter the watcher's row index by sidecar
|
|
732
|
+
// senderId, then partition into live vs archived for the log line
|
|
733
|
+
// (the merged list returns to the caller in startedAt-desc order).
|
|
734
|
+
// Rows without a sidecar carry `senderId: null` and never match
|
|
735
|
+
// unless includeHidden=1 is passed (the legacy pre-Task-260 archives
|
|
736
|
+
// case). Hidden rows are dropped unless includeHidden=1.
|
|
737
|
+
const payloads = [];
|
|
738
|
+
let liveCount = 0;
|
|
739
|
+
let archivedCount = 0;
|
|
740
|
+
let hiddenSkipped = 0;
|
|
711
741
|
for (const row of deps.watcher.all()) {
|
|
712
|
-
if (row.
|
|
742
|
+
if (row.senderId !== senderId && !(includeHidden && row.senderId === null))
|
|
713
743
|
continue;
|
|
714
|
-
if (
|
|
744
|
+
if (row.hidden && !includeHidden) {
|
|
745
|
+
hiddenSkipped += 1;
|
|
715
746
|
continue;
|
|
716
|
-
|
|
717
|
-
//
|
|
718
|
-
|
|
747
|
+
}
|
|
748
|
+
// Confirm the JSONL is actually on disk; row state can lag behind
|
|
749
|
+
// a delete during boot-seed timing edges. Live rows whose PID
|
|
750
|
+
// file exists pre-first-turn legitimately have no JSONL — keep
|
|
751
|
+
// them in the result.
|
|
752
|
+
const archivedJsonl = jsonlPathForSessionId(deps.spawnCwd, row.sessionId, { archived: true });
|
|
753
|
+
const topJsonl = jsonlPathForSessionId(deps.spawnCwd, row.sessionId);
|
|
754
|
+
if (row.state === 'archived' && !existsSync(archivedJsonl) && !existsSync(topJsonl))
|
|
719
755
|
continue;
|
|
720
|
-
|
|
756
|
+
payloads.push(toPayload(row));
|
|
757
|
+
if (row.state === 'live')
|
|
758
|
+
liveCount += 1;
|
|
759
|
+
else
|
|
760
|
+
archivedCount += 1;
|
|
721
761
|
}
|
|
722
|
-
deps.logger(`list-source live=${
|
|
723
|
-
|
|
724
|
-
merged.sort((a, b) => (a.startedAt < b.startedAt ? 1 : -1));
|
|
762
|
+
deps.logger(`[list-source] live=${liveCount} archived=${archivedCount} hidden-skipped=${hiddenSkipped}`);
|
|
763
|
+
payloads.sort((a, b) => (a.startedAt < b.startedAt ? 1 : -1));
|
|
725
764
|
timed(deps.logger, 'GET', '/list', 200, Date.now() - start);
|
|
726
|
-
return c.json(
|
|
765
|
+
return c.json(payloads, 200);
|
|
727
766
|
});
|
|
728
767
|
app.post('/resume', async (c) => {
|
|
729
768
|
const start = Date.now();
|
|
@@ -800,6 +839,19 @@ export function buildHttpApp(deps) {
|
|
|
800
839
|
if (archivedExists && !canonicalExists) {
|
|
801
840
|
try {
|
|
802
841
|
renameSync(archivedJsonl, canonicalJsonl);
|
|
842
|
+
// Task 260 — sidecar travels with the JSONL. Move the archived
|
|
843
|
+
// sidecar to the top-level path so the watcher picks it up on
|
|
844
|
+
// the next reconcile.
|
|
845
|
+
const archivedSidecar = sidecarPathForSessionId(deps.spawnCwd, sessionId, { archived: true });
|
|
846
|
+
const canonicalSidecar = sidecarPathForSessionId(deps.spawnCwd, sessionId);
|
|
847
|
+
if (existsSync(archivedSidecar)) {
|
|
848
|
+
try {
|
|
849
|
+
renameSync(archivedSidecar, canonicalSidecar);
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
deps.logger(`resume-sidecar-restore-failed sessionId=${sessionId.slice(0, 8)} message=${err instanceof Error ? err.message : String(err)}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
803
855
|
deps.logger(`resume-archive-restored sessionId=${sessionId.slice(0, 8)}`);
|
|
804
856
|
}
|
|
805
857
|
catch (err) {
|
|
@@ -837,7 +889,16 @@ export function buildHttpApp(deps) {
|
|
|
837
889
|
return c.json({ error: 'resume-restore-failed', detail: msg }, 500);
|
|
838
890
|
}
|
|
839
891
|
}
|
|
840
|
-
const result = await spawnClaudeSession({
|
|
892
|
+
const result = await spawnClaudeSession({
|
|
893
|
+
...deps,
|
|
894
|
+
watcher: deps.watcher,
|
|
895
|
+
onSessionReady: (tracker) => {
|
|
896
|
+
attachPublicAudit(deps, tracker.sessionId, role);
|
|
897
|
+
attachSpecialistEndTurnAutoArchive(deps, tracker.sessionId, tracker.specialist);
|
|
898
|
+
},
|
|
899
|
+
onUrlCaptured: notifyUrlCaptured,
|
|
900
|
+
tunnelUrlOverride,
|
|
901
|
+
}, {
|
|
841
902
|
senderId,
|
|
842
903
|
userId,
|
|
843
904
|
role: role,
|
|
@@ -867,11 +928,13 @@ export function buildHttpApp(deps) {
|
|
|
867
928
|
}
|
|
868
929
|
// Task 258 — the archive-restore-before-spawn flow means the operator's
|
|
869
930
|
// prior canonical sessionId IS the jsonl basename claude appends to.
|
|
870
|
-
// The
|
|
871
|
-
//
|
|
872
|
-
// directly. No mint divergence to log: there's only one sessionId.
|
|
931
|
+
// The PtyTracker's sessionId is set to that same prior canonical id
|
|
932
|
+
// (resumeClaudeSessionId), so the wire payload echoes it directly.
|
|
873
933
|
deps.logger(`resume pid=${result.session.pid} sessionId=${sessionId} resumeFrom=${sessionId} latency-ms=${Date.now() - start}`);
|
|
874
|
-
const payload = {
|
|
934
|
+
const payload = {
|
|
935
|
+
...buildSpawnReturnPayload(result.session.sessionId, result.session.pid, result.session.startedAt, result.session.url),
|
|
936
|
+
sessionId,
|
|
937
|
+
};
|
|
875
938
|
if (idempotencyKey) {
|
|
876
939
|
resumeDedupCache.set(`${senderId}:${idempotencyKey}`, { payload, createdAt: Date.now() });
|
|
877
940
|
}
|
|
@@ -890,30 +953,27 @@ export function buildHttpApp(deps) {
|
|
|
890
953
|
timed(deps.logger, 'GET', `/${id}/meta`, 404, Date.now() - start);
|
|
891
954
|
return c.json({ error: 'session-not-found' }, 404);
|
|
892
955
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
956
|
+
// Task 260 — single row source. /meta uses the deeper enumerator
|
|
957
|
+
// limit (jsonlReadMaxBytes) for richer turn/sizeBytes data than
|
|
958
|
+
// /list provides; otherwise the payload is identical to /list.
|
|
959
|
+
let payload = toPayload(row);
|
|
960
|
+
const candidatePath = row.state === 'archived'
|
|
961
|
+
? jsonlPathForSessionId(deps.spawnCwd, row.sessionId, { archived: true })
|
|
962
|
+
: jsonlPathForSessionId(deps.spawnCwd, row.sessionId);
|
|
963
|
+
if (existsSync(candidatePath)) {
|
|
964
|
+
const enriched = readJsonlSession(deps.spawnCwd, row.sessionId, deps.jsonlReadMaxBytes, deps.logger, deps.userTitleStore);
|
|
965
|
+
if (enriched) {
|
|
966
|
+
payload = {
|
|
967
|
+
...payload,
|
|
968
|
+
lastMessageAt: enriched.lastMessageAt,
|
|
969
|
+
turnCount: enriched.turnCount,
|
|
970
|
+
capped: enriched.capped,
|
|
971
|
+
sizeBytes: enriched.sizeBytes,
|
|
972
|
+
model: enriched.model,
|
|
973
|
+
displayName: enriched.displayName,
|
|
974
|
+
titleSource: enriched.titleSource,
|
|
975
|
+
};
|
|
912
976
|
}
|
|
913
|
-
payload = { ...base, lastMessageAt, turnCount, capped, sizeBytes, model };
|
|
914
|
-
}
|
|
915
|
-
else {
|
|
916
|
-
payload = toArchivedPayload(row, '');
|
|
917
977
|
}
|
|
918
978
|
deps.logger(`meta sessionId=${row.sessionId.slice(0, 8)} fields=${Object.keys(payload).length} ms=${Date.now() - start}`);
|
|
919
979
|
timed(deps.logger, 'GET', `/${id}/meta`, 200, Date.now() - start);
|
|
@@ -948,19 +1008,14 @@ export function buildHttpApp(deps) {
|
|
|
948
1008
|
const slugDir = join(claudeStateRoot(), 'projects', slug);
|
|
949
1009
|
const archiveDir = join(slugDir, 'archive');
|
|
950
1010
|
const fileName = `${row.sessionId}.jsonl`;
|
|
951
|
-
// Task
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
return false;
|
|
960
|
-
return jsonlPathForSessionId(deps.spawnCwd, s.sessionId) === candidateJsonl;
|
|
961
|
-
});
|
|
962
|
-
if (aliveOnPath) {
|
|
963
|
-
deps.logger(`archive-refused sessionId=${row.sessionId.slice(0, 8)} reason=pty-alive-jsonl-write store-key=${aliveOnPath.sessionId.slice(0, 8)}`);
|
|
1011
|
+
// Task 260 — refuse archive whenever an alive PTY exists for this
|
|
1012
|
+
// row in the manager's tracker map. Earlier this walked every
|
|
1013
|
+
// store entry to find one whose derived path equaled the candidate
|
|
1014
|
+
// — that loop was redundant because the JSONL basename IS the
|
|
1015
|
+
// sessionId, so the only relevant check is "is this sessionId
|
|
1016
|
+
// tracker alive?". `isLive` answers in O(1).
|
|
1017
|
+
if (mode === 'archive' && isLive(row.sessionId)) {
|
|
1018
|
+
deps.logger(`archive-refused sessionId=${row.sessionId.slice(0, 8)} reason=pty-alive`);
|
|
964
1019
|
timed(deps.logger, 'POST', `/${id}/archive`, 409, Date.now() - start);
|
|
965
1020
|
return c.json({ error: 'pty-still-alive', detail: 'end the session before archiving' }, 409);
|
|
966
1021
|
}
|
|
@@ -981,7 +1036,29 @@ export function buildHttpApp(deps) {
|
|
|
981
1036
|
timed(deps.logger, 'POST', `/${id}/archive`, 500, Date.now() - start);
|
|
982
1037
|
return c.json({ error: 'archive-failed', detail: msg }, 500);
|
|
983
1038
|
}
|
|
984
|
-
|
|
1039
|
+
// Task 260 — the sidecar travels with the JSONL. Best-effort: an
|
|
1040
|
+
// orphan sidecar at the source path is self-healing (the next
|
|
1041
|
+
// bootSeed indexes the row with null fields and the operator
|
|
1042
|
+
// re-archives), but the orphan is itself a regression signal the
|
|
1043
|
+
// log line surfaces.
|
|
1044
|
+
let sidecarMoved = false;
|
|
1045
|
+
try {
|
|
1046
|
+
const fromSidecar = mode === 'archive'
|
|
1047
|
+
? sidecarPathForSessionId(deps.spawnCwd, row.sessionId)
|
|
1048
|
+
: sidecarPathForSessionId(deps.spawnCwd, row.sessionId, { archived: true });
|
|
1049
|
+
const toSidecar = mode === 'archive'
|
|
1050
|
+
? sidecarPathForSessionId(deps.spawnCwd, row.sessionId, { archived: true })
|
|
1051
|
+
: sidecarPathForSessionId(deps.spawnCwd, row.sessionId);
|
|
1052
|
+
if (existsSync(fromSidecar)) {
|
|
1053
|
+
renameSync(fromSidecar, toSidecar);
|
|
1054
|
+
sidecarMoved = true;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
catch (err) {
|
|
1058
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1059
|
+
deps.logger(`archive-sidecar-move-failed sessionId=${row.sessionId.slice(0, 8)} mode=${mode} message=${msg}`);
|
|
1060
|
+
}
|
|
1061
|
+
deps.logger(`[archive] sessionId=${row.sessionId.slice(0, 8)} mode=${mode} from=${basename(fromJsonl)} to=${join(mode === 'archive' ? 'archive' : '.', basename(toJsonl))} sidecar-moved=${sidecarMoved}`);
|
|
985
1062
|
timed(deps.logger, 'POST', `/${id}/archive`, 200, Date.now() - start);
|
|
986
1063
|
return c.json({ ok: true, mode }, 200);
|
|
987
1064
|
});
|
|
@@ -1008,7 +1085,11 @@ export function buildHttpApp(deps) {
|
|
|
1008
1085
|
return c.json({ error: 'invalid-json-body' }, 400);
|
|
1009
1086
|
}
|
|
1010
1087
|
const row = resolveRow(deps.watcher, id);
|
|
1011
|
-
|
|
1088
|
+
// Task 260 — rename resolves via the watcher row index. The live
|
|
1089
|
+
// tracker is no longer a secondary lookup surface — the watcher's
|
|
1090
|
+
// PID-file event indexes every live session, so a sessionId that
|
|
1091
|
+
// names no row also names no live PTY.
|
|
1092
|
+
const sessionId = row?.sessionId;
|
|
1012
1093
|
if (!sessionId) {
|
|
1013
1094
|
timed(deps.logger, 'POST', `/${id}/rename`, 404, Date.now() - start);
|
|
1014
1095
|
return c.json({ error: 'session-not-found' }, 404);
|
|
@@ -1037,12 +1118,28 @@ export function buildHttpApp(deps) {
|
|
|
1037
1118
|
return c.json({ error: 'invalid-session-id' }, 400);
|
|
1038
1119
|
}
|
|
1039
1120
|
const row = resolveRow(deps.watcher, id);
|
|
1040
|
-
if (!row && !
|
|
1121
|
+
if (!row && !isLive(id)) {
|
|
1041
1122
|
timed(deps.logger, 'POST', `/${id}/stop`, 404, Date.now() - start);
|
|
1042
1123
|
return c.json({ error: 'session-not-found' }, 404);
|
|
1043
1124
|
}
|
|
1044
1125
|
const sessionId = row?.sessionId ?? id;
|
|
1045
|
-
await stopSession({
|
|
1126
|
+
const outcome = await stopSession({
|
|
1127
|
+
killGraceMs: deps.killGraceMs,
|
|
1128
|
+
logger: deps.logger,
|
|
1129
|
+
runSystemctl: deps.runSystemctl,
|
|
1130
|
+
}, sessionId);
|
|
1131
|
+
// Task 250 — surface scope-stop failures as 500 so the operator sees
|
|
1132
|
+
// the End-click fail when systemd cannot stop the unit. ok and
|
|
1133
|
+
// already-gone collapse to 204 (idempotent); no-store-entry is the
|
|
1134
|
+
// archived-row path that auto-archive triggers (no PTY to stop).
|
|
1135
|
+
if (outcome.scopeOutcome === 'error') {
|
|
1136
|
+
timed(deps.logger, 'POST', `/${sessionId}/stop`, 500, Date.now() - start);
|
|
1137
|
+
return c.json({
|
|
1138
|
+
error: 'scope-stop-failed',
|
|
1139
|
+
exitCode: outcome.exitCode,
|
|
1140
|
+
detail: 'systemctl --user stop returned a non-zero exit code for the session\'s scope unit',
|
|
1141
|
+
}, 500);
|
|
1142
|
+
}
|
|
1046
1143
|
timed(deps.logger, 'POST', `/${sessionId}/stop`, 204, Date.now() - start);
|
|
1047
1144
|
return c.body(null, 204);
|
|
1048
1145
|
});
|
|
@@ -1056,27 +1153,40 @@ export function buildHttpApp(deps) {
|
|
|
1056
1153
|
}
|
|
1057
1154
|
const row = resolveRow(deps.watcher, id);
|
|
1058
1155
|
const sessionId = row?.sessionId ?? id;
|
|
1059
|
-
if (
|
|
1156
|
+
if (isLive(sessionId)) {
|
|
1060
1157
|
deps.logger(`delete-rejected sessionId=${sessionId.slice(0, 8)} reason=pty-still-alive`);
|
|
1061
1158
|
timed(deps.logger, 'DELETE', `/${id}`, 409, Date.now() - start);
|
|
1062
1159
|
return c.json({ error: 'pty-still-alive', detail: 'stop the session before deleting' }, 409);
|
|
1063
1160
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1161
|
+
// Task 260 — purge from BOTH locations: a row may have its JSONL
|
|
1162
|
+
// at the top level (live or just-ended) or under archive/ (archived).
|
|
1163
|
+
const topJsonl = jsonlPathForSessionId(deps.spawnCwd, sessionId);
|
|
1164
|
+
const archivedJsonl = jsonlPathForSessionId(deps.spawnCwd, sessionId, { archived: true });
|
|
1165
|
+
const topSidecar = sidecarPathForSessionId(deps.spawnCwd, sessionId);
|
|
1166
|
+
const archivedSidecar = sidecarPathForSessionId(deps.spawnCwd, sessionId, { archived: true });
|
|
1167
|
+
const hasTop = existsSync(topJsonl);
|
|
1168
|
+
const hasArchived = existsSync(archivedJsonl);
|
|
1169
|
+
if (!row || (!hasTop && !hasArchived)) {
|
|
1066
1170
|
timed(deps.logger, 'DELETE', `/${id}`, 404, Date.now() - start);
|
|
1067
1171
|
return c.json({ error: 'session-not-found' }, 404);
|
|
1068
1172
|
}
|
|
1069
1173
|
const subdir = join(claudeStateRoot(), 'projects', projectSlugForCwd(deps.spawnCwd), sessionId);
|
|
1070
1174
|
let bytes = 0;
|
|
1071
1175
|
try {
|
|
1072
|
-
bytes =
|
|
1176
|
+
bytes = hasTop ? statSync(topJsonl).size : hasArchived ? statSync(archivedJsonl).size : 0;
|
|
1073
1177
|
}
|
|
1074
1178
|
catch {
|
|
1075
1179
|
// size is informational
|
|
1076
1180
|
}
|
|
1077
1181
|
try {
|
|
1078
|
-
if (
|
|
1079
|
-
rmSync(
|
|
1182
|
+
if (hasTop)
|
|
1183
|
+
rmSync(topJsonl);
|
|
1184
|
+
if (hasArchived)
|
|
1185
|
+
rmSync(archivedJsonl);
|
|
1186
|
+
if (existsSync(topSidecar))
|
|
1187
|
+
rmSync(topSidecar);
|
|
1188
|
+
if (existsSync(archivedSidecar))
|
|
1189
|
+
rmSync(archivedSidecar);
|
|
1080
1190
|
if (existsSync(subdir))
|
|
1081
1191
|
rmSync(subdir, { recursive: true, force: true });
|
|
1082
1192
|
}
|
|
@@ -1100,15 +1210,6 @@ export function buildHttpApp(deps) {
|
|
|
1100
1210
|
const id = c.req.param('sessionId');
|
|
1101
1211
|
const row = resolveRow(deps.watcher, id);
|
|
1102
1212
|
const sessionId = row?.sessionId ?? id;
|
|
1103
|
-
const s = deps.store.get(sessionId);
|
|
1104
|
-
if (!s) {
|
|
1105
|
-
timed(deps.logger, 'POST', `/${id}/input`, 404, Date.now() - start);
|
|
1106
|
-
return c.json({ error: 'session-not-found' }, 404);
|
|
1107
|
-
}
|
|
1108
|
-
if (s.exited) {
|
|
1109
|
-
timed(deps.logger, 'POST', `/${id}/input`, 410, Date.now() - start);
|
|
1110
|
-
return c.json({ error: 'session-exited' }, 410);
|
|
1111
|
-
}
|
|
1112
1213
|
let body;
|
|
1113
1214
|
try {
|
|
1114
1215
|
body = await c.req.json();
|
|
@@ -1121,50 +1222,42 @@ export function buildHttpApp(deps) {
|
|
|
1121
1222
|
timed(deps.logger, 'POST', `/${id}/input`, 400, Date.now() - start);
|
|
1122
1223
|
return c.json({ error: 'text-required' }, 400);
|
|
1123
1224
|
}
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
// payload as a single paste followed by an explicit submit keystroke.
|
|
1131
|
-
// Single-line text keeps the previous typed-keystroke path byte-for-byte
|
|
1132
|
-
// so the Sidebar timestamp stimulus is unchanged.
|
|
1133
|
-
const multiline = body.text.includes('\n');
|
|
1134
|
-
const submitMode = multiline ? 'bracketed-paste' : 'typed';
|
|
1135
|
-
try {
|
|
1136
|
-
if (multiline) {
|
|
1137
|
-
s.pty.write(`\x1b[200~${body.text}\x1b[201~\r`);
|
|
1225
|
+
const channel = row?.channel ?? null;
|
|
1226
|
+
const result = writeInputToPty(sessionId, body.text, deps.logger, channel);
|
|
1227
|
+
if (!result.ok) {
|
|
1228
|
+
if (result.error === 'not-found') {
|
|
1229
|
+
timed(deps.logger, 'POST', `/${id}/input`, 404, Date.now() - start);
|
|
1230
|
+
return c.json({ error: 'session-not-found' }, 404);
|
|
1138
1231
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1232
|
+
if (result.error === 'exited') {
|
|
1233
|
+
timed(deps.logger, 'POST', `/${id}/input`, 410, Date.now() - start);
|
|
1234
|
+
return c.json({ error: 'session-exited' }, 410);
|
|
1141
1235
|
}
|
|
1142
|
-
}
|
|
1143
|
-
catch (err) {
|
|
1144
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1145
|
-
deps.logger(`input-failed pid=${s.pid} sessionId=${sessionId} message=${msg}`);
|
|
1146
1236
|
timed(deps.logger, 'POST', `/${id}/input`, 500, Date.now() - start);
|
|
1147
|
-
return c.json({ error: 'pty-write-failed', detail:
|
|
1237
|
+
return c.json({ error: 'pty-write-failed', detail: result.detail }, 500);
|
|
1148
1238
|
}
|
|
1149
|
-
deps.logger(`input pid=${s.pid} sessionId=${sessionId} bytes=${bytes} channel=${s.channel} multiline=${multiline} submit-mode=${submitMode}`);
|
|
1150
1239
|
timed(deps.logger, 'POST', `/${id}/input`, 200, Date.now() - start);
|
|
1151
|
-
return c.json({ ok: true, bytes }, 200);
|
|
1240
|
+
return c.json({ ok: true, bytes: result.bytes }, 200);
|
|
1152
1241
|
});
|
|
1153
1242
|
app.get('/:sessionId/log', (c) => {
|
|
1154
1243
|
const start = Date.now();
|
|
1155
1244
|
const id = c.req.param('sessionId');
|
|
1156
1245
|
const row = resolveRow(deps.watcher, id);
|
|
1157
1246
|
const sessionId = row?.sessionId ?? id;
|
|
1158
|
-
const
|
|
1159
|
-
// Task
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1247
|
+
const live = isLive(sessionId);
|
|
1248
|
+
// Task 260 — /log reads from whichever location holds the JSONL
|
|
1249
|
+
// right now: top level for live + recently-ended rows, archive
|
|
1250
|
+
// subdir for archived rows. 404 when there's no row AND no live
|
|
1251
|
+
// PTY for this id; 202 pending when a live PTY hasn't flushed
|
|
1252
|
+
// its first turn yet (no JSONL on disk in either location).
|
|
1253
|
+
if (!live && !row) {
|
|
1163
1254
|
timed(deps.logger, 'GET', `/${id}/log`, 404, Date.now() - start);
|
|
1164
1255
|
return c.json({ error: 'session-not-found' }, 404);
|
|
1165
1256
|
}
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1257
|
+
const topJsonl = jsonlPathForSessionId(deps.spawnCwd, sessionId);
|
|
1258
|
+
const archivedJsonl = jsonlPathForSessionId(deps.spawnCwd, sessionId, { archived: true });
|
|
1259
|
+
const jsonlPath = existsSync(topJsonl) ? topJsonl : archivedJsonl;
|
|
1260
|
+
if (live && !existsSync(topJsonl) && !existsSync(archivedJsonl)) {
|
|
1168
1261
|
timed(deps.logger, 'GET', `/${id}/log`, 202, Date.now() - start);
|
|
1169
1262
|
return c.json({ jsonlPath: null, exists: false, sizeBytes: 0, pending: true }, 202);
|
|
1170
1263
|
}
|
|
@@ -1238,7 +1331,12 @@ export function buildHttpApp(deps) {
|
|
|
1238
1331
|
}
|
|
1239
1332
|
position = initialSize;
|
|
1240
1333
|
}
|
|
1241
|
-
|
|
1334
|
+
// Task 260 — prefer the row's PID (watcher-derived) but fall back
|
|
1335
|
+
// to the live tracker if the row hasn't been populated yet (the
|
|
1336
|
+
// pre-first-turn window during which the watcher's PID-file event
|
|
1337
|
+
// has not fired against the row builder).
|
|
1338
|
+
const followPid = row?.pid ?? getPtyTrackerForTests(sessionId)?.pid ?? null;
|
|
1339
|
+
deps.logger(`log-follow-open sessionId=${sessionId} initialBytes=${position} pid=${followPid ?? 'null'}`);
|
|
1242
1340
|
let active = true;
|
|
1243
1341
|
out.onAbort(() => {
|
|
1244
1342
|
active = false;
|
|
@@ -1270,7 +1368,7 @@ export function buildHttpApp(deps) {
|
|
|
1270
1368
|
deps.logger(`log-follow-close sessionId=${sessionId} reason=${closeReason} linesStreamed=${linesStreamed} ms=${Date.now() - followStreamStart}`);
|
|
1271
1369
|
});
|
|
1272
1370
|
});
|
|
1273
|
-
app.get('/healthz', (c) => c.json({ ok: true, sessions:
|
|
1371
|
+
app.get('/healthz', (c) => c.json({ ok: true, sessions: livePtyCount() }));
|
|
1274
1372
|
return app;
|
|
1275
1373
|
}
|
|
1276
1374
|
//# sourceMappingURL=http-server.js.map
|