@edspencer/paddock 0.66.0 → 0.66.2
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/package.json +1 -1
- package/packages/server/dist/app.d.ts +8 -0
- package/packages/server/dist/app.js +4 -1
- package/packages/server/dist/herdctl-jobs.d.ts +20 -0
- package/packages/server/dist/herdctl-jobs.js +94 -0
- package/packages/server/dist/herdctl.d.ts +46 -0
- package/packages/server/dist/herdctl.js +58 -0
- package/packages/server/dist/instance-config.d.ts +56 -5
- package/packages/server/dist/instance-config.js +206 -24
- package/packages/server/dist/project-paths.d.ts +46 -1
- package/packages/server/dist/project-paths.js +66 -2
- package/packages/server/dist/project-types.d.ts +23 -0
- package/packages/server/dist/project-types.js +35 -0
- package/packages/server/dist/projects.d.ts +5 -2
- package/packages/server/dist/projects.js +71 -17
- package/packages/server/dist/queued-message.d.ts +87 -0
- package/packages/server/dist/queued-message.js +214 -1
- package/packages/server/dist/routes/chats.js +27 -8
- package/packages/server/dist/routes/meta.js +32 -5
- package/packages/server/dist/routes/projects.js +71 -7
- package/packages/server/dist/session-hub.d.ts +69 -1
- package/packages/server/dist/session-hub.js +73 -1
- package/packages/server/dist/turn-interlock.js +8 -0
- package/packages/server/dist/ws-protocol.d.ts +128 -5
- package/packages/server/dist/ws-protocol.js +30 -13
- package/packages/server/dist/ws.d.ts +0 -52
- package/packages/server/dist/ws.js +545 -127
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/InstanceConfigPage-DKzAHOhx.js +2 -0
- package/packages/web/dist/assets/{MarkdownRenderer-Bn6VPBdL.js → MarkdownRenderer-DDVO0zhl.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-CQmDBtd-.js → ProjectRedirect-WczXTV0_.js} +1 -1
- package/packages/web/dist/assets/ProjectView-Dd_myWpV.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-BW56zqke.js → ProjectsGrid-BeJj0q6Y.js} +1 -1
- package/packages/web/dist/assets/{arc-CGy6WkLl.js → arc-DXqVnx_r.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-DJH1WIxY.js → architectureDiagram-3BPJPVTR-FgkfD1Bk.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Cufb_pvX.js → blockDiagram-GPEHLZMM-WwkIIJsI.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-glqeg1Ky.js → c4Diagram-AAUBKEIU-BIzkTibx.js} +1 -1
- package/packages/web/dist/assets/channel-DUsOl5_p.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-SOFTHYgh.js → chunk-2J33WTMH-C4TBH3R6.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DFxFAiG8.js → chunk-4BX2VUAB-DTKJZlRM.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CEEDxZVG.js → chunk-55IACEB6-BC9_Qc8h.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM--3eiptsh.js → chunk-727SXJPM-D3gQ4rP3.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-DIHIugCo.js → chunk-AQP2D5EJ-My2AXBvz.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-BmxWZl5c.js → chunk-FMBD7UC4-D09zE6Ow.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-hMHtJs_I.js → chunk-ND2GUHAM-CUSgeCLl.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CRnnasyy.js → chunk-QZHKN3VN-B6jVPYgy.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CIdDKEqH.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CIdDKEqH.js +2 -0
- package/packages/web/dist/assets/{core-CGW2CVAY.js → core-DPY0seI-.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-kcl9qSfK.js → cose-bilkent-S5V4N54A-BasCiARv.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-BiLsFx1W.js → dagre-BM42HDAG-Cimn11Qe.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BcIpoY66.js → diagram-2AECGRRQ-fe9cDHyn.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-BK5p5kJa.js → diagram-5GNKFQAL-T0tQlfbl.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-BmJYalCP.js → diagram-KO2AKTUF-dLORrSiu.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-BHs0SWEM.js → diagram-LMA3HP47-iUMeaR-J.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-CSs8hie4.js → diagram-OG6HWLK6-oEH6olHK.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-4qd_Td27.js → erDiagram-TEJ5UH35-VtQqF9SY.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-CstUgr7V.js → flowDiagram-I6XJVG4X-wvF2EkJa.js} +1 -1
- package/packages/web/dist/assets/{format-B_kAvAW3.js → format-CvRqOKZU.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DTn9OuIc.js → ganttDiagram-6RSMTGT7-ng4qJ0E1.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-E_bCor2O.js → gitGraphDiagram-PVQCEYII-BV4aWy8k.js} +1 -1
- package/packages/web/dist/assets/index-DGNSeqL-.js +71 -0
- package/packages/web/dist/assets/index-DMRbU1Jl.css +1 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-foRUrIOQ.js → infoDiagram-5YYISTIA-BpGTxAjU.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-CTON8v5t.js → ishikawaDiagram-YF4QCWOH-Cj36LyoS.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-D-qwGjr7.js → journeyDiagram-JHISSGLW-0vNlg3um.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-5l4y83rE.js → kanban-definition-UN3LZRKU-wijkEDaR.js} +1 -1
- package/packages/web/dist/assets/{linear-B1GiUWpI.js → linear-LqRymMD_.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-btQIOU6d.js → mermaid.core-DPrhSs95.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DduczfMv.js → mindmap-definition-RKZ34NQL-en-uGqgC.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-D86uGusc.js → pieDiagram-4H26LBE5-DKBZ2Vnx.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bo5EW3MZ.js → quadrantDiagram-W4KKPZXB--g-WtxyL.js} +1 -1
- package/packages/web/dist/assets/queued-CagZVfWU.js +2 -0
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-C9aZJK93.js → requirementDiagram-4Y6WPE33-Dl4eYxDI.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-b__W6SAJ.js → sankeyDiagram-5OEKKPKP-7lt4gqSw.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-Lr4Df8kX.js → sequenceDiagram-3UESZ5HK-COgq-dXg.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-R0XxGxD5.js → stateDiagram-AJRCARHV-CL6XC79S.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BDlRyphY.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-D42Je9LW.js → timeline-definition-PNZ67QCA-C5egKeGT.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-KCu14-QB.js → vennDiagram-CIIHVFJN-C_UhtC8u.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-Cfir3V9o.js → wardley-L42UT6IY-Bj9chUqz.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BkXjbTXu.js → wardleyDiagram-YWT4CUSO-D7VnZoRp.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-BVT60Pln.js → xychartDiagram-2RQKCTM6-VrWLDoi4.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/InstanceConfigPage-DBrwtZjx.js +0 -2
- package/packages/web/dist/assets/ProjectView-BVKu9oIg.js +0 -13
- package/packages/web/dist/assets/channel-C4kPYn_d.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DCg6gTVh.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DCg6gTVh.js +0 -2
- package/packages/web/dist/assets/index-B-QAOq9a.js +0 -71
- package/packages/web/dist/assets/index-B3K5nsLe.css +0 -1
- package/packages/web/dist/assets/queued-LamEU0GH.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-i2pZN-iR.js +0 -2
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ import { HerdctlService } from "./herdctl.js";
|
|
|
16
16
|
import { GitService } from "./git.js";
|
|
17
17
|
import { GithubAuth } from "./github-auth.js";
|
|
18
18
|
import { type Transcriber } from "./transcribe.js";
|
|
19
|
+
import type { SessionHub } from "./session-hub.js";
|
|
19
20
|
import { SweepService } from "./sweep.js";
|
|
20
21
|
import { ArchiveStore } from "./archive.js";
|
|
21
22
|
import { StarStore } from "./star.js";
|
|
@@ -49,6 +50,13 @@ export interface BuiltApp {
|
|
|
49
50
|
events: PaddockEventBus;
|
|
50
51
|
/** Unified trigger registry (Epic T / T1) — the sole trigger CRUD surface. */
|
|
51
52
|
triggers: TriggerService;
|
|
53
|
+
/**
|
|
54
|
+
* The WS layer's session hub — every in-flight turn, and the fan-out to the
|
|
55
|
+
* sockets watching it. Exposed for callers that need to observe or annotate a
|
|
56
|
+
* turn without holding a socket (the destructive-op interlock reaches it the
|
|
57
|
+
* same way, via the route context).
|
|
58
|
+
*/
|
|
59
|
+
hub: SessionHub;
|
|
52
60
|
/** Tear down the fleet + close the server (no process.exit, for tests). */
|
|
53
61
|
close: () => Promise<void>;
|
|
54
62
|
}
|
|
@@ -399,5 +399,8 @@ export async function buildApp(opts = {}) {
|
|
|
399
399
|
await herdctl.stop().catch(() => undefined);
|
|
400
400
|
await app.close().catch(() => undefined);
|
|
401
401
|
};
|
|
402
|
-
|
|
402
|
+
// `hub` rides along like every other built piece: it is the WS layer's turn
|
|
403
|
+
// bookkeeping, and the only place a caller can observe or annotate a turn
|
|
404
|
+
// without a live socket (see SessionHub.noteCancel).
|
|
405
|
+
return { app, cfg, projects, herdctl, git, githubAuth, sweep, archive, star, readState, unread, parentDetach, runProvenance, queuedMessage, transcriber, events, triggers, hub: chatHandler.managementOpsContext.hub, close };
|
|
403
406
|
}
|
|
@@ -72,6 +72,26 @@ export declare function listProjectRuns(stateDir: string, project: Project, limi
|
|
|
72
72
|
* view degrades to config-only rather than failing to render.
|
|
73
73
|
*/
|
|
74
74
|
export declare function listRunsForAgents(stateDir: string, agents: string[], limit?: number): Promise<JobMetadata[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Drop the job records of one or more CHATS (#732) — called when Paddock removes
|
|
77
|
+
* a chat from a project, so the record cannot outlive the transcript and keep
|
|
78
|
+
* the sidebar badge counting a chat that is no longer there to open.
|
|
79
|
+
*/
|
|
80
|
+
export declare function purgeSessionJobs(stateDir: string, sessionIds: string[]): Promise<number>;
|
|
81
|
+
/**
|
|
82
|
+
* Drop the job records of one or more AGENTS (#734) — called when a project is
|
|
83
|
+
* deleted, for every agent name that project owned (keeper, sweeper, hooks,
|
|
84
|
+
* triggers). Agent-keyed rather than project-keyed because the agent name is
|
|
85
|
+
* what the record actually carries.
|
|
86
|
+
*
|
|
87
|
+
* This is the containment half of the fix rather than the structural one: the
|
|
88
|
+
* durable answer to "a re-created project inherits the old one's records" is to
|
|
89
|
+
* key them by a stable project id instead of the user-controlled, reusable slug.
|
|
90
|
+
* That is a herdctl-side identity change (records are herdctl's format, written
|
|
91
|
+
* by its runtimes); purging on delete makes the leak impossible NOW without
|
|
92
|
+
* waiting for it, and stays correct afterwards.
|
|
93
|
+
*/
|
|
94
|
+
export declare function purgeAgentJobs(stateDir: string, agents: string[]): Promise<number>;
|
|
75
95
|
/**
|
|
76
96
|
* Point every herdctl job record for `sessionId` at the project's keeper so
|
|
77
97
|
* the core attribution index (last-write-wins per session) lists the session
|
|
@@ -138,6 +138,100 @@ export async function listRunsForAgents(stateDir, agents, limit = 200) {
|
|
|
138
138
|
const filtered = jobs.filter((j) => wanted.has(j.agent));
|
|
139
139
|
return limit > 0 ? filtered.slice(0, limit) : filtered;
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Delete every job record matching `match`, plus each one's sibling `.jsonl`
|
|
143
|
+
* output file. Returns how many records were removed.
|
|
144
|
+
*
|
|
145
|
+
* The shared inverse of the writes above (#732 / #734). The jobs directory is
|
|
146
|
+
* append-only and fleet-wide, and until now NOTHING ever removed a record — so a
|
|
147
|
+
* record outlived whatever it described. Two user-visible consequences, one
|
|
148
|
+
* mechanism:
|
|
149
|
+
*
|
|
150
|
+
* - A deleted CHAT kept feeding {@link lastTurnCompletedAtByProject}, i.e. the
|
|
151
|
+
* sidebar unread badge, with no chat left to open to clear it (#732).
|
|
152
|
+
* - A deleted PROJECT left its keeper's whole history behind, keyed by the
|
|
153
|
+
* agent name — which is derived from the slug, which is derived from the
|
|
154
|
+
* NAME. Re-creating a project called the same thing therefore inherited the
|
|
155
|
+
* previous incarnation's `/runs`, prompt text and reply summaries included
|
|
156
|
+
* (#734).
|
|
157
|
+
*
|
|
158
|
+
* Records are matched by reading them, not by parsing filenames: a job id
|
|
159
|
+
* encodes only a date and six characters of the session id, which is neither an
|
|
160
|
+
* agent nor a reliable session key.
|
|
161
|
+
*
|
|
162
|
+
* Best-effort per file, deliberately: a record we cannot read is a record we
|
|
163
|
+
* cannot prove is ours, so it is left alone rather than removed on a guess, and
|
|
164
|
+
* an unlink failure never fails the delete the user actually asked for. The
|
|
165
|
+
* caller invalidates {@link JobsDirIndex} afterwards — the index also drops
|
|
166
|
+
* vanished files on its own next scan, so this is belt-and-braces.
|
|
167
|
+
*/
|
|
168
|
+
async function purgeJobs(stateDir, match) {
|
|
169
|
+
const jobsDir = jobsDirOf(stateDir);
|
|
170
|
+
let entries = [];
|
|
171
|
+
try {
|
|
172
|
+
entries = await fs.readdir(jobsDir);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return 0; // no jobs dir yet — nothing to purge
|
|
176
|
+
}
|
|
177
|
+
let removed = 0;
|
|
178
|
+
for (const name of entries) {
|
|
179
|
+
if (!name.endsWith(".yaml"))
|
|
180
|
+
continue;
|
|
181
|
+
const file = path.join(jobsDir, name);
|
|
182
|
+
let parsed = null;
|
|
183
|
+
try {
|
|
184
|
+
parsed = YAML.parse(await fs.readFile(file, "utf8"));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!parsed || !match(parsed))
|
|
190
|
+
continue;
|
|
191
|
+
await fs.rm(file, { force: true }).catch(() => undefined);
|
|
192
|
+
// The record names its own output file; fall back to the `<id>.jsonl`
|
|
193
|
+
// convention {@link writeAgentAdoptionJob} writes when it does not.
|
|
194
|
+
const output = typeof parsed.output_file === "string" && parsed.output_file.length > 0
|
|
195
|
+
? parsed.output_file
|
|
196
|
+
: path.join(jobsDir, `${name.slice(0, -".yaml".length)}.jsonl`);
|
|
197
|
+
// Only ever unlink an output file that lives in the jobs dir we just read.
|
|
198
|
+
if (path.dirname(path.resolve(output)) === path.resolve(jobsDir)) {
|
|
199
|
+
await fs.rm(output, { force: true }).catch(() => undefined);
|
|
200
|
+
}
|
|
201
|
+
removed++;
|
|
202
|
+
}
|
|
203
|
+
return removed;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Drop the job records of one or more CHATS (#732) — called when Paddock removes
|
|
207
|
+
* a chat from a project, so the record cannot outlive the transcript and keep
|
|
208
|
+
* the sidebar badge counting a chat that is no longer there to open.
|
|
209
|
+
*/
|
|
210
|
+
export async function purgeSessionJobs(stateDir, sessionIds) {
|
|
211
|
+
if (sessionIds.length === 0)
|
|
212
|
+
return 0;
|
|
213
|
+
const wanted = new Set(sessionIds);
|
|
214
|
+
return purgeJobs(stateDir, (r) => typeof r.session_id === "string" && wanted.has(r.session_id));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Drop the job records of one or more AGENTS (#734) — called when a project is
|
|
218
|
+
* deleted, for every agent name that project owned (keeper, sweeper, hooks,
|
|
219
|
+
* triggers). Agent-keyed rather than project-keyed because the agent name is
|
|
220
|
+
* what the record actually carries.
|
|
221
|
+
*
|
|
222
|
+
* This is the containment half of the fix rather than the structural one: the
|
|
223
|
+
* durable answer to "a re-created project inherits the old one's records" is to
|
|
224
|
+
* key them by a stable project id instead of the user-controlled, reusable slug.
|
|
225
|
+
* That is a herdctl-side identity change (records are herdctl's format, written
|
|
226
|
+
* by its runtimes); purging on delete makes the leak impossible NOW without
|
|
227
|
+
* waiting for it, and stays correct afterwards.
|
|
228
|
+
*/
|
|
229
|
+
export async function purgeAgentJobs(stateDir, agents) {
|
|
230
|
+
if (agents.length === 0)
|
|
231
|
+
return 0;
|
|
232
|
+
const wanted = new Set(agents);
|
|
233
|
+
return purgeJobs(stateDir, (r) => typeof r.agent === "string" && wanted.has(r.agent));
|
|
234
|
+
}
|
|
141
235
|
/**
|
|
142
236
|
* Point every herdctl job record for `sessionId` at the project's keeper so
|
|
143
237
|
* the core attribution index (last-write-wins per session) lists the session
|
|
@@ -515,6 +515,52 @@ export declare class HerdctlService {
|
|
|
515
515
|
* — so `retained` is how a caller tells "gone" from "no longer shown here".
|
|
516
516
|
*/
|
|
517
517
|
deleteSession(agentName: string, sessionId: string): Promise<ChatDeletion>;
|
|
518
|
+
/**
|
|
519
|
+
* Drop the herdctl job records for chats that have left this instance (#732),
|
|
520
|
+
* so a record cannot outlive its transcript and keep the sidebar unread badge
|
|
521
|
+
* counting a chat there is no longer anything to open.
|
|
522
|
+
*
|
|
523
|
+
* Takes a LIST rather than one id, and is called by the delete routes rather
|
|
524
|
+
* than from inside {@link deleteSession}, for one reason: purging is a scan of
|
|
525
|
+
* the whole shared jobs directory, and the batch route deletes up to 500 chats
|
|
526
|
+
* in a loop. Per-chat purging would re-scan a 2,000-record directory 500 times
|
|
527
|
+
* (see `herdctl-jobs-index.ts` for what that costs). One pass, all ids.
|
|
528
|
+
*
|
|
529
|
+
* Callers pass only the chats whose transcript was actually REMOVED. Under
|
|
530
|
+
* `claude.transcripts: host` a delete RELEASES instead — the chat is still
|
|
531
|
+
* listed there, a known deferred gap (#693, asserted by
|
|
532
|
+
* `delete-chat-host-transcripts.test.ts`), and purging would close it as a
|
|
533
|
+
* side effect, because the job record is part of what re-attributes a
|
|
534
|
+
* structurally-discovered session to the keeper. Resolving somebody else's
|
|
535
|
+
* deferred decision by accident is not this change's business, and keeping the
|
|
536
|
+
* record for a chat that is still in the list is coherent anyway:
|
|
537
|
+
* `buildChatTurns` prunes against the live chat list, so the badge follows the
|
|
538
|
+
* list either way — and will stop counting the chat the moment #693 makes it
|
|
539
|
+
* leave.
|
|
540
|
+
*
|
|
541
|
+
* Best-effort: tidying state must never fail the delete the user asked for.
|
|
542
|
+
*/
|
|
543
|
+
purgeSessionJobs(sessionIds: string[]): Promise<void>;
|
|
544
|
+
/**
|
|
545
|
+
* Drop the herdctl job records of every agent a project owned (#734) — the
|
|
546
|
+
* records half of a project delete.
|
|
547
|
+
*
|
|
548
|
+
* Job records are keyed by AGENT NAME, and an agent name is derived from the
|
|
549
|
+
* slug, which is derived from the project NAME. So without this, deleting
|
|
550
|
+
* "Foo" and creating a new "Foo" hands the new project the old one's run
|
|
551
|
+
* history: prompts, reply summaries, and a phantom `chatTurns` entry that
|
|
552
|
+
* shows an unread badge on a project with zero chats. Files, `.chats/` and
|
|
553
|
+
* `read-state.json` were already cleaned; these were the one thing left.
|
|
554
|
+
*
|
|
555
|
+
* Takes the same hook/trigger name lists {@link removeProjectAgent} does, so
|
|
556
|
+
* the two together are the full inverse of the create flow — and so they stay
|
|
557
|
+
* in step: if a delete ever has to sweep DESCENDANT workspaces (nesting), both
|
|
558
|
+
* need the same widening, not just one. Matching is on the exact agent name,
|
|
559
|
+
* so deleting `foo` cannot touch a sibling `foo-bar`.
|
|
560
|
+
*
|
|
561
|
+
* Best-effort — the project directory is already gone by the time we get here.
|
|
562
|
+
*/
|
|
563
|
+
purgeProjectJobs(slug: string, hookNames?: string[], triggerNames?: string[]): Promise<void>;
|
|
518
564
|
/**
|
|
519
565
|
* Drop the agent-level session pointer when it names a transcript we just
|
|
520
566
|
* removed. Issue #730 — silent data loss, and the fix is one `rm` of a file
|
|
@@ -903,6 +903,64 @@ export class HerdctlService {
|
|
|
903
903
|
this.invalidateSessions(agentName);
|
|
904
904
|
return { removed: false, retained: true };
|
|
905
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Drop the herdctl job records for chats that have left this instance (#732),
|
|
908
|
+
* so a record cannot outlive its transcript and keep the sidebar unread badge
|
|
909
|
+
* counting a chat there is no longer anything to open.
|
|
910
|
+
*
|
|
911
|
+
* Takes a LIST rather than one id, and is called by the delete routes rather
|
|
912
|
+
* than from inside {@link deleteSession}, for one reason: purging is a scan of
|
|
913
|
+
* the whole shared jobs directory, and the batch route deletes up to 500 chats
|
|
914
|
+
* in a loop. Per-chat purging would re-scan a 2,000-record directory 500 times
|
|
915
|
+
* (see `herdctl-jobs-index.ts` for what that costs). One pass, all ids.
|
|
916
|
+
*
|
|
917
|
+
* Callers pass only the chats whose transcript was actually REMOVED. Under
|
|
918
|
+
* `claude.transcripts: host` a delete RELEASES instead — the chat is still
|
|
919
|
+
* listed there, a known deferred gap (#693, asserted by
|
|
920
|
+
* `delete-chat-host-transcripts.test.ts`), and purging would close it as a
|
|
921
|
+
* side effect, because the job record is part of what re-attributes a
|
|
922
|
+
* structurally-discovered session to the keeper. Resolving somebody else's
|
|
923
|
+
* deferred decision by accident is not this change's business, and keeping the
|
|
924
|
+
* record for a chat that is still in the list is coherent anyway:
|
|
925
|
+
* `buildChatTurns` prunes against the live chat list, so the badge follows the
|
|
926
|
+
* list either way — and will stop counting the chat the moment #693 makes it
|
|
927
|
+
* leave.
|
|
928
|
+
*
|
|
929
|
+
* Best-effort: tidying state must never fail the delete the user asked for.
|
|
930
|
+
*/
|
|
931
|
+
async purgeSessionJobs(sessionIds) {
|
|
932
|
+
await jobs.purgeSessionJobs(this.cfg.stateDir, sessionIds).catch(() => 0);
|
|
933
|
+
this.jobsIndexOrNull?.invalidate();
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Drop the herdctl job records of every agent a project owned (#734) — the
|
|
937
|
+
* records half of a project delete.
|
|
938
|
+
*
|
|
939
|
+
* Job records are keyed by AGENT NAME, and an agent name is derived from the
|
|
940
|
+
* slug, which is derived from the project NAME. So without this, deleting
|
|
941
|
+
* "Foo" and creating a new "Foo" hands the new project the old one's run
|
|
942
|
+
* history: prompts, reply summaries, and a phantom `chatTurns` entry that
|
|
943
|
+
* shows an unread badge on a project with zero chats. Files, `.chats/` and
|
|
944
|
+
* `read-state.json` were already cleaned; these were the one thing left.
|
|
945
|
+
*
|
|
946
|
+
* Takes the same hook/trigger name lists {@link removeProjectAgent} does, so
|
|
947
|
+
* the two together are the full inverse of the create flow — and so they stay
|
|
948
|
+
* in step: if a delete ever has to sweep DESCENDANT workspaces (nesting), both
|
|
949
|
+
* need the same widening, not just one. Matching is on the exact agent name,
|
|
950
|
+
* so deleting `foo` cannot touch a sibling `foo-bar`.
|
|
951
|
+
*
|
|
952
|
+
* Best-effort — the project directory is already gone by the time we get here.
|
|
953
|
+
*/
|
|
954
|
+
async purgeProjectJobs(slug, hookNames = [], triggerNames = []) {
|
|
955
|
+
const agents = [
|
|
956
|
+
keeperAgentName(slug),
|
|
957
|
+
sweeperAgentName(slug),
|
|
958
|
+
...hookNames.map((name) => hookAgentName(slug, name)),
|
|
959
|
+
...triggerNames.map((name) => triggerAgentName(slug, name)),
|
|
960
|
+
];
|
|
961
|
+
await jobs.purgeAgentJobs(this.cfg.stateDir, agents).catch(() => 0);
|
|
962
|
+
this.jobsIndexOrNull?.invalidate();
|
|
963
|
+
}
|
|
906
964
|
/**
|
|
907
965
|
* Drop the agent-level session pointer when it names a transcript we just
|
|
908
966
|
* removed. Issue #730 — silent data loss, and the fix is one `rm` of a file
|
|
@@ -74,7 +74,17 @@ export interface InstanceConfigFieldDto {
|
|
|
74
74
|
help?: string;
|
|
75
75
|
type: FieldType;
|
|
76
76
|
enumValues?: readonly string[];
|
|
77
|
+
/** EFFECTIVE now: the value the running (frozen) process resolved at boot. */
|
|
77
78
|
value: unknown;
|
|
79
|
+
/**
|
|
80
|
+
* PENDING: what this field would resolve to if the process restarted right
|
|
81
|
+
* now — i.e. what is in `paddock.config.yaml` this instant (or the built-in
|
|
82
|
+
* default where the file says nothing, or `value` where an env var wins). This
|
|
83
|
+
* is what the editor renders and what a save round-trips through (#722).
|
|
84
|
+
*/
|
|
85
|
+
pendingValue: unknown;
|
|
86
|
+
/** `pendingValue` differs from `value` — the file has diverged from the process. */
|
|
87
|
+
pendingRestart: boolean;
|
|
78
88
|
default: unknown;
|
|
79
89
|
editable: boolean;
|
|
80
90
|
sensitive: boolean;
|
|
@@ -92,9 +102,33 @@ export interface InstanceConfigDto {
|
|
|
92
102
|
groups: InstanceConfigGroupDto[];
|
|
93
103
|
/** Absolute path of the file a PUT writes to (informational). */
|
|
94
104
|
configPath: string;
|
|
95
|
-
/**
|
|
96
|
-
|
|
105
|
+
/**
|
|
106
|
+
* At least one editable field's `pendingValue` differs from its `value`: the
|
|
107
|
+
* file on disk no longer describes the running process, and a restart would
|
|
108
|
+
* change behaviour. Was hardcoded `false` before #722 — which meant nothing
|
|
109
|
+
* ever told an operator their own save was unapplied.
|
|
110
|
+
*/
|
|
111
|
+
restartRequired: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Fingerprint of the config file as read for THIS response (`null` when the
|
|
114
|
+
* file does not exist yet). A client echoes it back as `expectedVersion` on
|
|
115
|
+
* the PUT; the server refuses the write if the file changed meanwhile, so two
|
|
116
|
+
* tabs conflict loudly instead of silently last-writer-winning (#722).
|
|
117
|
+
*/
|
|
118
|
+
configVersion: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Set when the file exists but could not be read or parsed. Pending values are
|
|
121
|
+
* unknowable in that state, so they fall back to the effective ones — better
|
|
122
|
+
* to say why than to silently report "nothing pending".
|
|
123
|
+
*/
|
|
124
|
+
configFileError?: string;
|
|
97
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The current fingerprint of the config file (`null` when absent) — the PUT
|
|
128
|
+
* path's half of the optimistic-concurrency check. Deliberately re-reads rather
|
|
129
|
+
* than trusting a cached value: the point is to notice a write we did not make.
|
|
130
|
+
*/
|
|
131
|
+
export declare function instanceConfigVersion(configPath: string): string | null;
|
|
98
132
|
/**
|
|
99
133
|
* Resolve the path a PUT writes to: an explicit `PADDOCK_CONFIG` env var wins
|
|
100
134
|
* (the same rule {@link import("./config.js").loadConfigFile} reads it back
|
|
@@ -102,9 +136,25 @@ export interface InstanceConfigDto {
|
|
|
102
136
|
*/
|
|
103
137
|
export declare function instanceConfigPath(cfg: PaddockConfig): string;
|
|
104
138
|
/**
|
|
105
|
-
* Build the grouped GET DTO from the resolved (frozen) config
|
|
106
|
-
*
|
|
107
|
-
*
|
|
139
|
+
* Build the grouped GET DTO from the resolved (frozen) config, the live env AND
|
|
140
|
+
* the config file on disk. Each field reports:
|
|
141
|
+
*
|
|
142
|
+
* - `value` — EFFECTIVE now, out of the frozen boot config;
|
|
143
|
+
* - `pendingValue` — what a restart would resolve, out of the file this instant;
|
|
144
|
+
* - `pendingRestart` — those two differ.
|
|
145
|
+
*
|
|
146
|
+
* Reading the file is the load-bearing part (#722). While this built the DTO
|
|
147
|
+
* from the frozen config alone, `GET` could not observe ANY write — including
|
|
148
|
+
* the one the caller had just made — so a successful save appeared to revert,
|
|
149
|
+
* two tabs clobbered each other with nothing able to notice, and
|
|
150
|
+
* `restartRequired` had no choice but to be a hardcoded `false`.
|
|
151
|
+
*
|
|
152
|
+
* Pending values are computed for EDITABLE, non-env-shadowed fields only:
|
|
153
|
+
* - an env-shadowed field resolves to the same env value after a restart, so
|
|
154
|
+
* the file cannot make it diverge;
|
|
155
|
+
* - the read-only `advanced` bindings are normalised at boot (`dataDir` and
|
|
156
|
+
* friends are canonicalised, `port` is `Number()`-ed), so comparing them
|
|
157
|
+
* against raw file text would manufacture divergence that isn't there.
|
|
108
158
|
*/
|
|
109
159
|
export declare function buildInstanceConfig(cfg: PaddockConfig): InstanceConfigDto;
|
|
110
160
|
/** A rejected PUT: which field failed and why (surfaced as a 400). */
|
|
@@ -117,6 +167,7 @@ export declare class InstanceConfigError extends Error {
|
|
|
117
167
|
* {@link InstanceConfigError} on the first offending field:
|
|
118
168
|
* - an unknown key,
|
|
119
169
|
* - a read-only / non-editable key,
|
|
170
|
+
* - a key an env var currently shadows,
|
|
120
171
|
* - a value the field's `coerce` rejects.
|
|
121
172
|
* Returns the coerced `{ key, value }` pairs ready to write. A field whose value
|
|
122
173
|
* is `null` (an editable optional being cleared) is returned with `value: null`
|