@edspencer/paddock 0.0.1 → 0.56.0
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/README.md +69 -0
- package/install-notice.mjs +29 -0
- package/package.json +58 -5
- package/packages/server/dist/adoptable.d.ts +127 -0
- package/packages/server/dist/adoptable.js +315 -0
- package/packages/server/dist/app.d.ts +67 -0
- package/packages/server/dist/app.js +325 -0
- package/packages/server/dist/archive.d.ts +42 -0
- package/packages/server/dist/archive.js +125 -0
- package/packages/server/dist/attachments-config.d.ts +114 -0
- package/packages/server/dist/attachments-config.js +165 -0
- package/packages/server/dist/attachments-hint.d.ts +53 -0
- package/packages/server/dist/attachments-hint.js +140 -0
- package/packages/server/dist/attachments.d.ts +33 -0
- package/packages/server/dist/attachments.js +117 -0
- package/packages/server/dist/auth.d.ts +84 -0
- package/packages/server/dist/auth.js +247 -0
- package/packages/server/dist/background.d.ts +36 -0
- package/packages/server/dist/background.js +137 -0
- package/packages/server/dist/bind-safety.d.ts +56 -0
- package/packages/server/dist/bind-safety.js +51 -0
- package/packages/server/dist/brand.d.ts +44 -0
- package/packages/server/dist/brand.js +88 -0
- package/packages/server/dist/chat-dto.d.ts +210 -0
- package/packages/server/dist/chat-dto.js +308 -0
- package/packages/server/dist/cli/import-chats.d.ts +21 -0
- package/packages/server/dist/cli/import-chats.js +197 -0
- package/packages/server/dist/cli/paddock.d.ts +2 -0
- package/packages/server/dist/cli/paddock.js +247 -0
- package/packages/server/dist/concurrency.d.ts +57 -0
- package/packages/server/dist/concurrency.js +71 -0
- package/packages/server/dist/config.d.ts +512 -0
- package/packages/server/dist/config.js +593 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/event-bus.d.ts +87 -0
- package/packages/server/dist/event-bus.js +48 -0
- package/packages/server/dist/git.d.ts +128 -0
- package/packages/server/dist/git.js +446 -0
- package/packages/server/dist/github-auth.d.ts +44 -0
- package/packages/server/dist/github-auth.js +169 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
- package/packages/server/dist/herdctl-agent-config.js +331 -0
- package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
- package/packages/server/dist/herdctl-agent-names.js +228 -0
- package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
- package/packages/server/dist/herdctl-jobs-index.js +225 -0
- package/packages/server/dist/herdctl-jobs.d.ts +98 -0
- package/packages/server/dist/herdctl-jobs.js +225 -0
- package/packages/server/dist/herdctl.d.ts +572 -0
- package/packages/server/dist/herdctl.js +1218 -0
- package/packages/server/dist/hook-config.d.ts +217 -0
- package/packages/server/dist/hook-config.js +224 -0
- package/packages/server/dist/http-bytes.d.ts +24 -0
- package/packages/server/dist/http-bytes.js +52 -0
- package/packages/server/dist/index.d.ts +1 -0
- package/packages/server/dist/index.js +14 -0
- package/packages/server/dist/instance-config.d.ts +135 -0
- package/packages/server/dist/instance-config.js +345 -0
- package/packages/server/dist/localcommand.d.ts +23 -0
- package/packages/server/dist/localcommand.js +140 -0
- package/packages/server/dist/management-auth.d.ts +99 -0
- package/packages/server/dist/management-auth.js +165 -0
- package/packages/server/dist/management-config.d.ts +164 -0
- package/packages/server/dist/management-config.js +286 -0
- package/packages/server/dist/management-mcp-server.d.ts +20 -0
- package/packages/server/dist/management-mcp-server.js +95 -0
- package/packages/server/dist/management-metadata.d.ts +67 -0
- package/packages/server/dist/management-metadata.js +56 -0
- package/packages/server/dist/management-ops.d.ts +110 -0
- package/packages/server/dist/management-ops.js +449 -0
- package/packages/server/dist/management-policy.d.ts +172 -0
- package/packages/server/dist/management-policy.js +259 -0
- package/packages/server/dist/message-provenance.d.ts +112 -0
- package/packages/server/dist/message-provenance.js +260 -0
- package/packages/server/dist/models.d.ts +164 -0
- package/packages/server/dist/models.js +208 -0
- package/packages/server/dist/openapi.d.ts +66 -0
- package/packages/server/dist/openapi.js +183 -0
- package/packages/server/dist/parent-detach.d.ts +30 -0
- package/packages/server/dist/parent-detach.js +109 -0
- package/packages/server/dist/preload.d.ts +43 -0
- package/packages/server/dist/preload.js +55 -0
- package/packages/server/dist/project-files.d.ts +76 -0
- package/packages/server/dist/project-files.js +191 -0
- package/packages/server/dist/project-mime.d.ts +35 -0
- package/packages/server/dist/project-mime.js +55 -0
- package/packages/server/dist/project-paths.d.ts +88 -0
- package/packages/server/dist/project-paths.js +126 -0
- package/packages/server/dist/project-types.d.ts +316 -0
- package/packages/server/dist/project-types.js +48 -0
- package/packages/server/dist/projects.d.ts +202 -0
- package/packages/server/dist/projects.js +894 -0
- package/packages/server/dist/queued-message.d.ts +35 -0
- package/packages/server/dist/queued-message.js +116 -0
- package/packages/server/dist/read-state.d.ts +46 -0
- package/packages/server/dist/read-state.js +139 -0
- package/packages/server/dist/recovery-config.d.ts +111 -0
- package/packages/server/dist/recovery-config.js +105 -0
- package/packages/server/dist/recovery.d.ts +262 -0
- package/packages/server/dist/recovery.js +490 -0
- package/packages/server/dist/resume-drain.d.ts +122 -0
- package/packages/server/dist/resume-drain.js +158 -0
- package/packages/server/dist/route-context.d.ts +168 -0
- package/packages/server/dist/route-context.js +39 -0
- package/packages/server/dist/route-errors.d.ts +8 -0
- package/packages/server/dist/route-errors.js +13 -0
- package/packages/server/dist/routes/chats.d.ts +12 -0
- package/packages/server/dist/routes/chats.js +1406 -0
- package/packages/server/dist/routes/git.d.ts +13 -0
- package/packages/server/dist/routes/git.js +236 -0
- package/packages/server/dist/routes/mcp.d.ts +83 -0
- package/packages/server/dist/routes/mcp.js +250 -0
- package/packages/server/dist/routes/meta.d.ts +15 -0
- package/packages/server/dist/routes/meta.js +447 -0
- package/packages/server/dist/routes/projects.d.ts +25 -0
- package/packages/server/dist/routes/projects.js +736 -0
- package/packages/server/dist/routes/triggers.d.ts +21 -0
- package/packages/server/dist/routes/triggers.js +324 -0
- package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
- package/packages/server/dist/routes/workspace-mount.js +32 -0
- package/packages/server/dist/routes.d.ts +26 -0
- package/packages/server/dist/routes.js +29 -0
- package/packages/server/dist/run-provenance.d.ts +109 -0
- package/packages/server/dist/run-provenance.js +206 -0
- package/packages/server/dist/runs.d.ts +92 -0
- package/packages/server/dist/runs.js +72 -0
- package/packages/server/dist/schedule-config.d.ts +68 -0
- package/packages/server/dist/schedule-config.js +75 -0
- package/packages/server/dist/schedule-session.d.ts +34 -0
- package/packages/server/dist/schedule-session.js +120 -0
- package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
- package/packages/server/dist/self-mcp-descriptions.js +145 -0
- package/packages/server/dist/self-mcp-read.d.ts +15 -0
- package/packages/server/dist/self-mcp-read.js +97 -0
- package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
- package/packages/server/dist/self-mcp-triggers.js +144 -0
- package/packages/server/dist/self-mcp-types.d.ts +248 -0
- package/packages/server/dist/self-mcp-types.js +10 -0
- package/packages/server/dist/self-mcp-util.d.ts +73 -0
- package/packages/server/dist/self-mcp-util.js +157 -0
- package/packages/server/dist/self-mcp-write.d.ts +35 -0
- package/packages/server/dist/self-mcp-write.js +215 -0
- package/packages/server/dist/self-mcp.d.ts +95 -0
- package/packages/server/dist/self-mcp.js +448 -0
- package/packages/server/dist/send-file-mcp.d.ts +46 -0
- package/packages/server/dist/send-file-mcp.js +281 -0
- package/packages/server/dist/session-hub.d.ts +186 -0
- package/packages/server/dist/session-hub.js +291 -0
- package/packages/server/dist/spawn-capability.d.ts +99 -0
- package/packages/server/dist/spawn-capability.js +106 -0
- package/packages/server/dist/star.d.ts +26 -0
- package/packages/server/dist/star.js +94 -0
- package/packages/server/dist/start.d.ts +1 -0
- package/packages/server/dist/start.js +21 -0
- package/packages/server/dist/subagents.d.ts +337 -0
- package/packages/server/dist/subagents.js +626 -0
- package/packages/server/dist/sweep.d.ts +122 -0
- package/packages/server/dist/sweep.js +696 -0
- package/packages/server/dist/tooldetails.d.ts +33 -0
- package/packages/server/dist/tooldetails.js +466 -0
- package/packages/server/dist/transcribe.d.ts +69 -0
- package/packages/server/dist/transcribe.js +119 -0
- package/packages/server/dist/transcripts.d.ts +42 -0
- package/packages/server/dist/transcripts.js +164 -0
- package/packages/server/dist/trigger-config.d.ts +278 -0
- package/packages/server/dist/trigger-config.js +417 -0
- package/packages/server/dist/trigger-runtime.d.ts +94 -0
- package/packages/server/dist/trigger-runtime.js +82 -0
- package/packages/server/dist/trigger-session.d.ts +34 -0
- package/packages/server/dist/trigger-session.js +122 -0
- package/packages/server/dist/triggers.d.ts +83 -0
- package/packages/server/dist/triggers.js +124 -0
- package/packages/server/dist/trusted-proxy.d.ts +57 -0
- package/packages/server/dist/trusted-proxy.js +157 -0
- package/packages/server/dist/turn-notice.d.ts +154 -0
- package/packages/server/dist/turn-notice.js +271 -0
- package/packages/server/dist/unread.d.ts +36 -0
- package/packages/server/dist/unread.js +123 -0
- package/packages/server/dist/usage.d.ts +66 -0
- package/packages/server/dist/usage.js +243 -0
- package/packages/server/dist/wake-injection.d.ts +156 -0
- package/packages/server/dist/wake-injection.js +136 -0
- package/packages/server/dist/whisper-local.d.ts +9 -0
- package/packages/server/dist/whisper-local.js +122 -0
- package/packages/server/dist/ws-context.d.ts +158 -0
- package/packages/server/dist/ws-context.js +1 -0
- package/packages/server/dist/ws-protocol.d.ts +423 -0
- package/packages/server/dist/ws-protocol.js +251 -0
- package/packages/server/dist/ws-self-mcp.d.ts +52 -0
- package/packages/server/dist/ws-self-mcp.js +28 -0
- package/packages/server/dist/ws-triggers.d.ts +29 -0
- package/packages/server/dist/ws-triggers.js +261 -0
- package/packages/server/dist/ws-turn.d.ts +86 -0
- package/packages/server/dist/ws-turn.js +751 -0
- package/packages/server/dist/ws.d.ts +62 -0
- package/packages/server/dist/ws.js +1024 -0
- package/packages/server/package.json +6 -0
- package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-CJZFLj_8.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-BHwVbbqB.js +2 -0
- package/packages/web/dist/assets/ProjectView-BOvuAITg.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-BCojZOGY.js +2 -0
- package/packages/web/dist/assets/arc-BVyxvnkl.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-Cd9Puqg1.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-B_AQt_Rx.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-7e0TQtXB.js +11 -0
- package/packages/web/dist/assets/channel-C_1aISNn.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-Dz4WZmUZ.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-DVWYldo6.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-BIMrizQd.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-CL2q9fF1.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-BSq3HlQ9.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-Bgt64pLE.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-C8Pr2A38.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-BAOqh-MN.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +2 -0
- package/packages/web/dist/assets/core-lJeGACQg.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-BF4tameA.js +2 -0
- package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
- package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
- package/packages/web/dist/assets/css-DazXZka4.js +2 -0
- package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
- package/packages/web/dist/assets/dagre-BM42HDAG-DdsLM6ql.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-BQ78S_RM.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-CAKE6nHE.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-fEvHRXgr.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-CtWsgeSm.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-Buog8k7_.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-CWvVVPZS.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-DsCGXtp4.js +163 -0
- package/packages/web/dist/assets/format-DamJQ9LO.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-CsdwIvvC.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-hZvrX1sD.js +107 -0
- package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
- package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
- package/packages/web/dist/assets/index-CGbZdrm8.js +71 -0
- package/packages/web/dist/assets/index-DJDQ210-.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-nE_Vtz_l.js +3 -0
- package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
- package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
- package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js +71 -0
- package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
- package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
- package/packages/web/dist/assets/journeyDiagram-JHISSGLW-mjU1fMc4.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-C_9xNaa3.js +90 -0
- package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
- package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
- package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
- package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
- package/packages/web/dist/assets/linear-BxKd91ho.js +2 -0
- package/packages/web/dist/assets/mermaid.core-C-CuhD8I.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bs6yfcZv.js +97 -0
- package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
- package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
- package/packages/web/dist/assets/pieDiagram-4H26LBE5-DNdqX7O5.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-RmxMdm2q.js +85 -0
- package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
- package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
- package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-OAAhWcfK.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-BWF7rCyN.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-M3b_zqqo.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-BpOCh63N.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-D_iPH45Q.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-DVD9I2Om.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-Bn1IKwjn.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-DwtI0bkR.js +8 -0
- package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
- package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/packages/web/dist/icons/apple-touch-icon.png +0 -0
- package/packages/web/dist/icons/favicon-16.png +0 -0
- package/packages/web/dist/icons/favicon-32.png +0 -0
- package/packages/web/dist/icons/icon-192.png +0 -0
- package/packages/web/dist/icons/icon-512.png +0 -0
- package/packages/web/dist/icons/maskable-512.png +0 -0
- package/packages/web/dist/index.html +56 -0
- package/packages/web/dist/manifest.webmanifest +32 -0
- package/packages/web/dist/sw.js +139 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/** Sentinel model the CLI stamps on its synthetic placeholder turns. Mirrors
|
|
2
|
+
* `@herdctl/chat`'s `SYNTHETIC_MODEL` (kept local to avoid a value import churn). */
|
|
3
|
+
export declare const SYNTHETIC_MODEL = "<synthetic>";
|
|
4
|
+
/** The three notice families a dead-ended turn resolves to. */
|
|
5
|
+
export type TurnNoticeKind = "usage_limit" | "error" | "max_turns";
|
|
6
|
+
/**
|
|
7
|
+
* A surfaced turn-ending condition. `message` is the one-line human summary the
|
|
8
|
+
* banner shows; `resetTime` (usage-limit only) is the parsed "resets …" clause;
|
|
9
|
+
* `retryable` says whether offering a Continue/Retry affordance is safe (a usage
|
|
10
|
+
* limit is NOT retryable — it only clears when the quota resets).
|
|
11
|
+
*/
|
|
12
|
+
export interface TurnNotice {
|
|
13
|
+
kind: TurnNoticeKind;
|
|
14
|
+
message: string;
|
|
15
|
+
/** Usage-limit only: the reset clause, e.g. `"7:10pm (America/New_York)"`. */
|
|
16
|
+
resetTime?: string;
|
|
17
|
+
/** Optional secondary detail (e.g. the raw SDK error subtype / message). */
|
|
18
|
+
detail?: string;
|
|
19
|
+
/** Whether a Continue/Retry affordance is safe to offer for this notice. */
|
|
20
|
+
retryable: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Loosely-typed shape of the fields we read off an SDK message / JSONL entry. */
|
|
23
|
+
interface RawMessage {
|
|
24
|
+
type?: string;
|
|
25
|
+
subtype?: string;
|
|
26
|
+
is_error?: boolean;
|
|
27
|
+
success?: boolean;
|
|
28
|
+
message?: {
|
|
29
|
+
role?: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
content?: unknown;
|
|
32
|
+
stop_reason?: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Classify a synthetic assistant message's text.
|
|
37
|
+
*
|
|
38
|
+
* Returns a `usage_limit` {@link TurnNotice} for a "session/usage limit" message
|
|
39
|
+
* (with the reset clause parsed out), the sentinel `"declined"` for the
|
|
40
|
+
* runtime's own `"No response requested."` (and any other non-limit synthetic —
|
|
41
|
+
* a placeholder the UI should simply suppress, never render), or `null` for
|
|
42
|
+
* empty text.
|
|
43
|
+
*/
|
|
44
|
+
export declare function classifySyntheticText(text: string): TurnNotice | "declined" | null;
|
|
45
|
+
/**
|
|
46
|
+
* Classify a terminal `type:"result"` SDK message. Returns an `error`/`max_turns`
|
|
47
|
+
* {@link TurnNotice} when the result marks a genuine failure, else `null` for a
|
|
48
|
+
* successful turn.
|
|
49
|
+
*
|
|
50
|
+
* The authoritative failure signal is the **subtype** — the SDK's error results
|
|
51
|
+
* always carry an `error_*` subtype (`error_during_execution`, `error_max_turns`,
|
|
52
|
+
* …) — or an explicit `success === false`. This mirrors `@herdctl/core`'s own
|
|
53
|
+
* success computation (`chatSession`: `subtype.startsWith("error") || success ===
|
|
54
|
+
* false`), so Paddock's banner never disagrees with herdctl about whether a turn
|
|
55
|
+
* failed.
|
|
56
|
+
*
|
|
57
|
+
* We DELIBERATELY do NOT treat `is_error === true` as a failure on its own
|
|
58
|
+
* (issue #329 regression): `SDKResultSuccess` is typed `is_error: boolean`, and
|
|
59
|
+
* the runtime stamps `is_error:true` (with `api_error_status`) on a
|
|
60
|
+
* `subtype:"success"` result when the turn RECOVERED from a transient mid-turn
|
|
61
|
+
* API error (e.g. "Connection closed mid-response") and still produced a normal
|
|
62
|
+
* reply. Reading that as a turn failure rendered a false "turn failed" banner
|
|
63
|
+
* directly beneath a perfectly good reply on essentially every session-mode turn.
|
|
64
|
+
* A genuine error always carries an `error_*` subtype, so nothing is lost.
|
|
65
|
+
*/
|
|
66
|
+
export declare function classifyResult(result: {
|
|
67
|
+
subtype?: string;
|
|
68
|
+
is_error?: boolean;
|
|
69
|
+
success?: boolean;
|
|
70
|
+
}): TurnNotice | null;
|
|
71
|
+
/** Build a plain `error` notice from a free-text turn-failure message (a thrown
|
|
72
|
+
* error / a failed `chat:complete` that never reached a `result`). */
|
|
73
|
+
export declare function errorNotice(message: string | undefined): TurnNotice;
|
|
74
|
+
/**
|
|
75
|
+
* Classify one raw SDK/JSONL message. Surfaces a {@link TurnNotice} for a
|
|
76
|
+
* synthetic usage-limit assistant message or a terminal error result; returns
|
|
77
|
+
* `null` for everything else — ordinary output, a successful result, and every
|
|
78
|
+
* suppressed synthetic placeholder (`"No response requested."`).
|
|
79
|
+
*/
|
|
80
|
+
export declare function noticeFromMessage(m: RawMessage): TurnNotice | null;
|
|
81
|
+
/**
|
|
82
|
+
* Did this streamed message emit REAL assistant prose — a non-synthetic assistant
|
|
83
|
+
* message carrying non-empty **text** content, regardless of `stop_reason`?
|
|
84
|
+
*
|
|
85
|
+
* The live paths (`ws.ts`) OR-accumulate this across every message of a turn (a
|
|
86
|
+
* per-turn `producedReply` flag) so a terminal error/`max_turns` result that
|
|
87
|
+
* arrives AFTER the turn already showed the user real prose does not paint a false
|
|
88
|
+
* "turn failed" banner beneath the answer (issues #380, #394). In session mode the
|
|
89
|
+
* SDK's terminal `result` frame — streamed live but never persisted — can carry an
|
|
90
|
+
* `error_*` subtype / `success:false` even though the turn produced a perfectly
|
|
91
|
+
* good reply; on reload the history path ({@link scanTranscriptNotice}) suppresses
|
|
92
|
+
* the dead-end via its "any real assistant text supersedes the notice" rule, and
|
|
93
|
+
* this predicate lets the live path apply the SAME rule.
|
|
94
|
+
*
|
|
95
|
+
* #394 — why this is TEXT-ANY-STOP_REASON, not text+`end_turn`. The original #380
|
|
96
|
+
* predicate required a SINGLE message with non-empty text AND `stop_reason:
|
|
97
|
+
* "end_turn"`. Long tool-driven turns break both halves: (a) the visible prose
|
|
98
|
+
* usually rides on a message that ALSO makes a tool call → `stop_reason:"tool_use"`
|
|
99
|
+
* (excluded by the old gate), and (b) the terminal `end_turn` message is frequently
|
|
100
|
+
* **thinking-only** (zero text). So `producedReply` never flipped true and the
|
|
101
|
+
* benign terminal error result was surfaced. Keying off ANY non-synthetic assistant
|
|
102
|
+
* text (across all of the turn's messages, no `stop_reason` gate) makes a
|
|
103
|
+
* text+`tool_use` message count and exactly matches `scanTranscriptNotice`, killing
|
|
104
|
+
* the live/history asymmetry.
|
|
105
|
+
*
|
|
106
|
+
* DELIBERATELY text-only (a bare `thinking` block does NOT count): this mirrors the
|
|
107
|
+
* history path — which clears only on real text — so a genuinely empty turn (no
|
|
108
|
+
* assistant text ANYWHERE, e.g. an `error_max_turns` that wrote nothing renderable)
|
|
109
|
+
* still surfaces its error notice.
|
|
110
|
+
*/
|
|
111
|
+
export declare function messageProducedReply(m: RawMessage): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Should an already-classified {@link TurnNotice} be dropped because a complete
|
|
114
|
+
* reply already streamed this turn (issue #380)? An `error` / `max_turns`
|
|
115
|
+
* dead-end beneath a good answer is a false alarm — suppress it. A `usage_limit`
|
|
116
|
+
* is NOT suppressed: a session-limit stop is a real dead-end still worth showing
|
|
117
|
+
* even alongside text.
|
|
118
|
+
*/
|
|
119
|
+
export declare function suppressNoticeAfterReply(notice: TurnNotice, producedReply: boolean): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Did this turn effectively succeed for the purposes of its post-turn side
|
|
122
|
+
* effects — the queue drain, the after-turn curation sweep, and the recovery
|
|
123
|
+
* watch (issue #404)?
|
|
124
|
+
*
|
|
125
|
+
* The raw `result.success` handed back by the drive is computed purely from the
|
|
126
|
+
* terminal `result` frame (`!error_* subtype && !success:false`). In session
|
|
127
|
+
* mode that frame routinely carries an `error_*` subtype / `success:false` even
|
|
128
|
+
* when a complete reply already streamed — the very asymmetry the #380/#394
|
|
129
|
+
* banner fix exists for. Gating side effects on the raw signal silently strands
|
|
130
|
+
* a queued follow-up (the flush that {@link drainQueue} owns never runs), skips
|
|
131
|
+
* the sweep, and drops the recovery arm.
|
|
132
|
+
*
|
|
133
|
+
* This is the side-effect twin of {@link suppressNoticeAfterReply}: both trust
|
|
134
|
+
* the same `producedReply` signal (a real assistant reply streamed this turn) to
|
|
135
|
+
* override a benign trailing failure. A turn is effectively successful when the
|
|
136
|
+
* raw signal says so OR a real reply was produced. A genuinely dead turn (no
|
|
137
|
+
* reply — a Stop, an up-front API error) keeps `false`, so its queue is held.
|
|
138
|
+
*/
|
|
139
|
+
export declare function turnEffectivelySucceeded(rawSuccess: boolean, producedReply: boolean): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Scan a transcript JSONL file for a TRAILING dead-end notice to surface on
|
|
142
|
+
* reload (the history-hydration path). Only the synthetic usage-limit case is
|
|
143
|
+
* recoverable from history: `@herdctl/core` drops synthetic messages when it
|
|
144
|
+
* parses the transcript, and an `error_max_turns` turn writes NOTHING at all —
|
|
145
|
+
* so those are live-only.
|
|
146
|
+
*
|
|
147
|
+
* A usage-limit notice is surfaced only when it is the LAST thing that happened
|
|
148
|
+
* on the transcript: a later real assistant reply (a non-synthetic assistant
|
|
149
|
+
* message with text) means the chat recovered, so any earlier limit is cleared.
|
|
150
|
+
* Returns the trailing notice, or `null`. Never throws (a missing/unreadable
|
|
151
|
+
* file → `null`).
|
|
152
|
+
*/
|
|
153
|
+
export declare function scanTranscriptNotice(filePath: string): Promise<TurnNotice | null>;
|
|
154
|
+
export {};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-notice classification (issue #329)
|
|
3
|
+
*
|
|
4
|
+
* A keeper turn can end without producing a normal assistant reply, and today
|
|
5
|
+
* those endings are INVISIBLE in the UI — the chat just looks dead. Two families:
|
|
6
|
+
*
|
|
7
|
+
* 1. A **synthetic** runtime message. The Claude Code CLI answers a turn it
|
|
8
|
+
* cannot really run with an assistant message tagged `model:"<synthetic>"`
|
|
9
|
+
* and `stop_reason:"stop_sequence"`. The most important one on this box is
|
|
10
|
+
* the shared Max-plan quota: `"You've hit your session limit · resets …"`.
|
|
11
|
+
* The interleaved `"No response requested."` placeholders are the runtime
|
|
12
|
+
* declining its own follow-up turns under the limit. `@herdctl/chat`'s
|
|
13
|
+
* translator DROPS every synthetic message (so no text ever streams), and
|
|
14
|
+
* `@herdctl/core`'s history parser drops them too — hence the silent stop.
|
|
15
|
+
*
|
|
16
|
+
* 2. A terminal **error result**. The Claude Agent SDK ends a turn with a
|
|
17
|
+
* `type:"result"` message whose `subtype`/`is_error` mark a failure:
|
|
18
|
+
* `error_max_turns` (the turn hit its max-turns cap and wrote NOTHING
|
|
19
|
+
* renderable to the transcript), `error_during_execution`, an API/overload
|
|
20
|
+
* error, etc.
|
|
21
|
+
*
|
|
22
|
+
* This module turns either signal into a small {@link TurnNotice} DTO that the
|
|
23
|
+
* live path (`ws.ts` per-message inspection) and the history-hydration path
|
|
24
|
+
* (`scanTranscriptNotice`) both surface to the client as a distinct notice turn.
|
|
25
|
+
* Pure + unit-tested; no I/O except {@link scanTranscriptNotice}.
|
|
26
|
+
*/
|
|
27
|
+
import { createReadStream } from "node:fs";
|
|
28
|
+
import { createInterface } from "node:readline";
|
|
29
|
+
/** Sentinel model the CLI stamps on its synthetic placeholder turns. Mirrors
|
|
30
|
+
* `@herdctl/chat`'s `SYNTHETIC_MODEL` (kept local to avoid a value import churn). */
|
|
31
|
+
export const SYNTHETIC_MODEL = "<synthetic>";
|
|
32
|
+
/** Flatten an assistant `content` (string, or an array of text blocks) to text. */
|
|
33
|
+
function contentText(content) {
|
|
34
|
+
if (typeof content === "string")
|
|
35
|
+
return content;
|
|
36
|
+
if (Array.isArray(content)) {
|
|
37
|
+
return content
|
|
38
|
+
.map((b) => (b && typeof b === "object" && typeof b.text === "string"
|
|
39
|
+
? b.text
|
|
40
|
+
: ""))
|
|
41
|
+
.join("");
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Classify a synthetic assistant message's text.
|
|
47
|
+
*
|
|
48
|
+
* Returns a `usage_limit` {@link TurnNotice} for a "session/usage limit" message
|
|
49
|
+
* (with the reset clause parsed out), the sentinel `"declined"` for the
|
|
50
|
+
* runtime's own `"No response requested."` (and any other non-limit synthetic —
|
|
51
|
+
* a placeholder the UI should simply suppress, never render), or `null` for
|
|
52
|
+
* empty text.
|
|
53
|
+
*/
|
|
54
|
+
export function classifySyntheticText(text) {
|
|
55
|
+
const trimmed = text.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return null;
|
|
58
|
+
// The shared Max-plan quota. Match on "session limit" / "usage limit" rather
|
|
59
|
+
// than the exact string so a reworded CLI message still surfaces.
|
|
60
|
+
if (/\b(session|usage) limit\b/i.test(trimmed) || /hit your .*limit/i.test(trimmed)) {
|
|
61
|
+
const resetTime = /resets?\s+(.+?)\s*$/i.exec(trimmed)?.[1]?.trim() || undefined;
|
|
62
|
+
return {
|
|
63
|
+
kind: "usage_limit",
|
|
64
|
+
message: trimmed,
|
|
65
|
+
...(resetTime ? { resetTime } : {}),
|
|
66
|
+
retryable: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// "No response requested." and any other synthetic placeholder: not a limit,
|
|
70
|
+
// carries no real output — suppress it (never a chat bubble).
|
|
71
|
+
return "declined";
|
|
72
|
+
}
|
|
73
|
+
/** Human-readable message for a terminal error `result`'s subtype. */
|
|
74
|
+
function errorMessageForSubtype(subtype) {
|
|
75
|
+
if (subtype === "error_max_turns") {
|
|
76
|
+
return {
|
|
77
|
+
kind: "max_turns",
|
|
78
|
+
message: "Claude reached its turn limit before finishing this turn.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
kind: "error",
|
|
83
|
+
message: "Claude's turn failed before producing a reply.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Classify a terminal `type:"result"` SDK message. Returns an `error`/`max_turns`
|
|
88
|
+
* {@link TurnNotice} when the result marks a genuine failure, else `null` for a
|
|
89
|
+
* successful turn.
|
|
90
|
+
*
|
|
91
|
+
* The authoritative failure signal is the **subtype** — the SDK's error results
|
|
92
|
+
* always carry an `error_*` subtype (`error_during_execution`, `error_max_turns`,
|
|
93
|
+
* …) — or an explicit `success === false`. This mirrors `@herdctl/core`'s own
|
|
94
|
+
* success computation (`chatSession`: `subtype.startsWith("error") || success ===
|
|
95
|
+
* false`), so Paddock's banner never disagrees with herdctl about whether a turn
|
|
96
|
+
* failed.
|
|
97
|
+
*
|
|
98
|
+
* We DELIBERATELY do NOT treat `is_error === true` as a failure on its own
|
|
99
|
+
* (issue #329 regression): `SDKResultSuccess` is typed `is_error: boolean`, and
|
|
100
|
+
* the runtime stamps `is_error:true` (with `api_error_status`) on a
|
|
101
|
+
* `subtype:"success"` result when the turn RECOVERED from a transient mid-turn
|
|
102
|
+
* API error (e.g. "Connection closed mid-response") and still produced a normal
|
|
103
|
+
* reply. Reading that as a turn failure rendered a false "turn failed" banner
|
|
104
|
+
* directly beneath a perfectly good reply on essentially every session-mode turn.
|
|
105
|
+
* A genuine error always carries an `error_*` subtype, so nothing is lost.
|
|
106
|
+
*/
|
|
107
|
+
export function classifyResult(result) {
|
|
108
|
+
const subtype = typeof result.subtype === "string" ? result.subtype : undefined;
|
|
109
|
+
// A `subtype:"success"` result is a completed turn, full stop. `is_error` only
|
|
110
|
+
// counts as a failure when the runtime gave us NO subtype at all (defensive:
|
|
111
|
+
// some other runtime shape that reports is_error without one).
|
|
112
|
+
const failed = (subtype !== undefined && subtype.startsWith("error")) ||
|
|
113
|
+
result.success === false ||
|
|
114
|
+
(subtype === undefined && result.is_error === true);
|
|
115
|
+
if (!failed)
|
|
116
|
+
return null;
|
|
117
|
+
const { kind, message } = errorMessageForSubtype(subtype);
|
|
118
|
+
return {
|
|
119
|
+
kind,
|
|
120
|
+
message,
|
|
121
|
+
...(subtype ? { detail: subtype } : {}),
|
|
122
|
+
retryable: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Build a plain `error` notice from a free-text turn-failure message (a thrown
|
|
126
|
+
* error / a failed `chat:complete` that never reached a `result`). */
|
|
127
|
+
export function errorNotice(message) {
|
|
128
|
+
const detail = message?.trim();
|
|
129
|
+
return {
|
|
130
|
+
kind: "error",
|
|
131
|
+
message: "Claude's turn failed before producing a reply.",
|
|
132
|
+
...(detail ? { detail } : {}),
|
|
133
|
+
retryable: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Classify one raw SDK/JSONL message. Surfaces a {@link TurnNotice} for a
|
|
138
|
+
* synthetic usage-limit assistant message or a terminal error result; returns
|
|
139
|
+
* `null` for everything else — ordinary output, a successful result, and every
|
|
140
|
+
* suppressed synthetic placeholder (`"No response requested."`).
|
|
141
|
+
*/
|
|
142
|
+
export function noticeFromMessage(m) {
|
|
143
|
+
if (m.type === "assistant" && m.message?.model === SYNTHETIC_MODEL) {
|
|
144
|
+
const r = classifySyntheticText(contentText(m.message?.content));
|
|
145
|
+
return r && r !== "declined" ? r : null;
|
|
146
|
+
}
|
|
147
|
+
if (m.type === "result") {
|
|
148
|
+
return classifyResult(m);
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Did this streamed message emit REAL assistant prose — a non-synthetic assistant
|
|
154
|
+
* message carrying non-empty **text** content, regardless of `stop_reason`?
|
|
155
|
+
*
|
|
156
|
+
* The live paths (`ws.ts`) OR-accumulate this across every message of a turn (a
|
|
157
|
+
* per-turn `producedReply` flag) so a terminal error/`max_turns` result that
|
|
158
|
+
* arrives AFTER the turn already showed the user real prose does not paint a false
|
|
159
|
+
* "turn failed" banner beneath the answer (issues #380, #394). In session mode the
|
|
160
|
+
* SDK's terminal `result` frame — streamed live but never persisted — can carry an
|
|
161
|
+
* `error_*` subtype / `success:false` even though the turn produced a perfectly
|
|
162
|
+
* good reply; on reload the history path ({@link scanTranscriptNotice}) suppresses
|
|
163
|
+
* the dead-end via its "any real assistant text supersedes the notice" rule, and
|
|
164
|
+
* this predicate lets the live path apply the SAME rule.
|
|
165
|
+
*
|
|
166
|
+
* #394 — why this is TEXT-ANY-STOP_REASON, not text+`end_turn`. The original #380
|
|
167
|
+
* predicate required a SINGLE message with non-empty text AND `stop_reason:
|
|
168
|
+
* "end_turn"`. Long tool-driven turns break both halves: (a) the visible prose
|
|
169
|
+
* usually rides on a message that ALSO makes a tool call → `stop_reason:"tool_use"`
|
|
170
|
+
* (excluded by the old gate), and (b) the terminal `end_turn` message is frequently
|
|
171
|
+
* **thinking-only** (zero text). So `producedReply` never flipped true and the
|
|
172
|
+
* benign terminal error result was surfaced. Keying off ANY non-synthetic assistant
|
|
173
|
+
* text (across all of the turn's messages, no `stop_reason` gate) makes a
|
|
174
|
+
* text+`tool_use` message count and exactly matches `scanTranscriptNotice`, killing
|
|
175
|
+
* the live/history asymmetry.
|
|
176
|
+
*
|
|
177
|
+
* DELIBERATELY text-only (a bare `thinking` block does NOT count): this mirrors the
|
|
178
|
+
* history path — which clears only on real text — so a genuinely empty turn (no
|
|
179
|
+
* assistant text ANYWHERE, e.g. an `error_max_turns` that wrote nothing renderable)
|
|
180
|
+
* still surfaces its error notice.
|
|
181
|
+
*/
|
|
182
|
+
export function messageProducedReply(m) {
|
|
183
|
+
if (m.type !== "assistant")
|
|
184
|
+
return false;
|
|
185
|
+
if (m.message?.model === SYNTHETIC_MODEL)
|
|
186
|
+
return false;
|
|
187
|
+
return contentText(m.message?.content).trim().length > 0;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Should an already-classified {@link TurnNotice} be dropped because a complete
|
|
191
|
+
* reply already streamed this turn (issue #380)? An `error` / `max_turns`
|
|
192
|
+
* dead-end beneath a good answer is a false alarm — suppress it. A `usage_limit`
|
|
193
|
+
* is NOT suppressed: a session-limit stop is a real dead-end still worth showing
|
|
194
|
+
* even alongside text.
|
|
195
|
+
*/
|
|
196
|
+
export function suppressNoticeAfterReply(notice, producedReply) {
|
|
197
|
+
return producedReply && (notice.kind === "error" || notice.kind === "max_turns");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Did this turn effectively succeed for the purposes of its post-turn side
|
|
201
|
+
* effects — the queue drain, the after-turn curation sweep, and the recovery
|
|
202
|
+
* watch (issue #404)?
|
|
203
|
+
*
|
|
204
|
+
* The raw `result.success` handed back by the drive is computed purely from the
|
|
205
|
+
* terminal `result` frame (`!error_* subtype && !success:false`). In session
|
|
206
|
+
* mode that frame routinely carries an `error_*` subtype / `success:false` even
|
|
207
|
+
* when a complete reply already streamed — the very asymmetry the #380/#394
|
|
208
|
+
* banner fix exists for. Gating side effects on the raw signal silently strands
|
|
209
|
+
* a queued follow-up (the flush that {@link drainQueue} owns never runs), skips
|
|
210
|
+
* the sweep, and drops the recovery arm.
|
|
211
|
+
*
|
|
212
|
+
* This is the side-effect twin of {@link suppressNoticeAfterReply}: both trust
|
|
213
|
+
* the same `producedReply` signal (a real assistant reply streamed this turn) to
|
|
214
|
+
* override a benign trailing failure. A turn is effectively successful when the
|
|
215
|
+
* raw signal says so OR a real reply was produced. A genuinely dead turn (no
|
|
216
|
+
* reply — a Stop, an up-front API error) keeps `false`, so its queue is held.
|
|
217
|
+
*/
|
|
218
|
+
export function turnEffectivelySucceeded(rawSuccess, producedReply) {
|
|
219
|
+
return rawSuccess || producedReply;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Scan a transcript JSONL file for a TRAILING dead-end notice to surface on
|
|
223
|
+
* reload (the history-hydration path). Only the synthetic usage-limit case is
|
|
224
|
+
* recoverable from history: `@herdctl/core` drops synthetic messages when it
|
|
225
|
+
* parses the transcript, and an `error_max_turns` turn writes NOTHING at all —
|
|
226
|
+
* so those are live-only.
|
|
227
|
+
*
|
|
228
|
+
* A usage-limit notice is surfaced only when it is the LAST thing that happened
|
|
229
|
+
* on the transcript: a later real assistant reply (a non-synthetic assistant
|
|
230
|
+
* message with text) means the chat recovered, so any earlier limit is cleared.
|
|
231
|
+
* Returns the trailing notice, or `null`. Never throws (a missing/unreadable
|
|
232
|
+
* file → `null`).
|
|
233
|
+
*/
|
|
234
|
+
export async function scanTranscriptNotice(filePath) {
|
|
235
|
+
let pending = null;
|
|
236
|
+
try {
|
|
237
|
+
const rl = createInterface({
|
|
238
|
+
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
239
|
+
crlfDelay: Infinity,
|
|
240
|
+
});
|
|
241
|
+
for await (const line of rl) {
|
|
242
|
+
const trimmed = line.trim();
|
|
243
|
+
if (!trimmed)
|
|
244
|
+
continue;
|
|
245
|
+
let entry;
|
|
246
|
+
try {
|
|
247
|
+
entry = JSON.parse(trimmed);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (entry.type !== "assistant")
|
|
253
|
+
continue;
|
|
254
|
+
const model = entry.message?.model;
|
|
255
|
+
if (model === SYNTHETIC_MODEL) {
|
|
256
|
+
const r = classifySyntheticText(contentText(entry.message?.content));
|
|
257
|
+
if (r && r !== "declined")
|
|
258
|
+
pending = r; // a usage-limit dead-end
|
|
259
|
+
// a "declined" placeholder neither sets nor clears the pending notice
|
|
260
|
+
}
|
|
261
|
+
else if (contentText(entry.message?.content).trim().length > 0) {
|
|
262
|
+
// A real assistant reply: the turn was actually answered → clear.
|
|
263
|
+
pending = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return pending;
|
|
271
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare class UnreadStore {
|
|
2
|
+
private readonly stateFile;
|
|
3
|
+
/** In-memory set of manually-unread keys (loaded once, written through on change). */
|
|
4
|
+
private marked;
|
|
5
|
+
/**
|
|
6
|
+
* The in-flight load, cached so concurrent first-callers share ONE read — same
|
|
7
|
+
* lost-update guard as {@link StarStore} / {@link ReadStateStore}.
|
|
8
|
+
*/
|
|
9
|
+
private loadPromise;
|
|
10
|
+
/** Serialises concurrent writes so the file never interleaves. */
|
|
11
|
+
private writing;
|
|
12
|
+
constructor(dataDir: string);
|
|
13
|
+
/** Load the persisted set (lazily, deduped; tolerant of a missing/corrupt file). */
|
|
14
|
+
private ensureLoaded;
|
|
15
|
+
/**
|
|
16
|
+
* Whether this user has manually flagged this chat unread. Non-throwing: any
|
|
17
|
+
* load error reads as false. Pass `null` for `username` to read the shared bucket.
|
|
18
|
+
*/
|
|
19
|
+
isUnread(username: string | null, agent: string, sessionId: string): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Set (or clear) this user's manual-unread flag for this chat, persisting the
|
|
22
|
+
* change. Idempotent. Returns whether the flag actually CHANGED, mirroring
|
|
23
|
+
* `StarStore.setStarred`. Pass `null` for `username` to write the shared bucket.
|
|
24
|
+
*/
|
|
25
|
+
setUnread(username: string | null, agent: string, sessionId: string, unread: boolean): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Set (or clear) this user's manual-unread flag for MANY chats in ONE write
|
|
28
|
+
* (#508) — the subtree "mark read / mark unread" action. Same single-persist
|
|
29
|
+
* rationale as `ArchiveStore.setManyArchived`: a client-side loop over a
|
|
30
|
+
* 21-chat family can half-succeed and leave the family in two different states.
|
|
31
|
+
* Returns the ids whose flag actually changed.
|
|
32
|
+
*/
|
|
33
|
+
setManyUnread(username: string | null, agent: string, sessionIds: readonly string[], unread: boolean): Promise<string[]>;
|
|
34
|
+
/** Write-through, serialised so overlapping toggles can't corrupt the file. */
|
|
35
|
+
private persist;
|
|
36
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnreadStore — a Paddock-side sidecar for the per-chat, per-user MANUAL "unread"
|
|
3
|
+
* override (#458).
|
|
4
|
+
*
|
|
5
|
+
* The unread affordance (#160) is normally DERIVED: a chat reads as unread when
|
|
6
|
+
* the agent finished a turn (`lastTurnCompletedAt`) more recently than the user
|
|
7
|
+
* last viewed it (`ReadStateStore` `lastSeen`). This store adds an explicit,
|
|
8
|
+
* user-driven override on top of that derivation: "mark this chat unread" so it
|
|
9
|
+
* resurfaces its cue the next time you come back to the list — the email-client
|
|
10
|
+
* pattern. It cannot be expressed by pushing `lastSeen` backwards, because that
|
|
11
|
+
* value is deliberately MONOTONIC (see {@link ReadStateStore}); it needs its own
|
|
12
|
+
* flag.
|
|
13
|
+
*
|
|
14
|
+
* Shape/pattern is a near-verbatim copy of {@link StarStore} — a tiny JSON
|
|
15
|
+
* sidecar in the data dir, write-through, serialised, corruption-tolerant, only
|
|
16
|
+
* storing the SET keys (an unread=false clears the key) so the file stays compact.
|
|
17
|
+
* The one difference is KEYING: like read-state (and unlike star/archive, which
|
|
18
|
+
* are shared) the override is PER USER — "I haven't dealt with this yet" is
|
|
19
|
+
* personal — so it reuses {@link keyOf} from read-state:
|
|
20
|
+
* - Real identity (trusted-header / jwt): `username \0 agent \0 sessionId`
|
|
21
|
+
* - No user (`none` mode / anonymous): `agent \0 sessionId` (shared bucket)
|
|
22
|
+
*
|
|
23
|
+
* Written `0o600` (like read-state) — low-sensitivity personal metadata (which
|
|
24
|
+
* chats you flagged to revisit), but no reason for it to be world-readable.
|
|
25
|
+
*/
|
|
26
|
+
import { promises as fs } from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { keyOf } from "./read-state.js";
|
|
29
|
+
const STATE_FILE = "unread-state.json";
|
|
30
|
+
export class UnreadStore {
|
|
31
|
+
stateFile;
|
|
32
|
+
/** In-memory set of manually-unread keys (loaded once, written through on change). */
|
|
33
|
+
marked = null;
|
|
34
|
+
/**
|
|
35
|
+
* The in-flight load, cached so concurrent first-callers share ONE read — same
|
|
36
|
+
* lost-update guard as {@link StarStore} / {@link ReadStateStore}.
|
|
37
|
+
*/
|
|
38
|
+
loadPromise = null;
|
|
39
|
+
/** Serialises concurrent writes so the file never interleaves. */
|
|
40
|
+
writing = Promise.resolve();
|
|
41
|
+
constructor(dataDir) {
|
|
42
|
+
this.stateFile = path.join(dataDir, STATE_FILE);
|
|
43
|
+
}
|
|
44
|
+
/** Load the persisted set (lazily, deduped; tolerant of a missing/corrupt file). */
|
|
45
|
+
ensureLoaded() {
|
|
46
|
+
if (this.marked)
|
|
47
|
+
return Promise.resolve(this.marked);
|
|
48
|
+
if (!this.loadPromise) {
|
|
49
|
+
this.loadPromise = (async () => {
|
|
50
|
+
let keys = [];
|
|
51
|
+
try {
|
|
52
|
+
const raw = await fs.readFile(this.stateFile, "utf8");
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (Array.isArray(parsed))
|
|
55
|
+
keys = parsed.filter((k) => typeof k === "string");
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* missing or unreadable — start empty */
|
|
59
|
+
}
|
|
60
|
+
this.marked = new Set(keys);
|
|
61
|
+
return this.marked;
|
|
62
|
+
})();
|
|
63
|
+
}
|
|
64
|
+
return this.loadPromise;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether this user has manually flagged this chat unread. Non-throwing: any
|
|
68
|
+
* load error reads as false. Pass `null` for `username` to read the shared bucket.
|
|
69
|
+
*/
|
|
70
|
+
async isUnread(username, agent, sessionId) {
|
|
71
|
+
const set = await this.ensureLoaded().catch(() => new Set());
|
|
72
|
+
return set.has(keyOf(username, agent, sessionId));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Set (or clear) this user's manual-unread flag for this chat, persisting the
|
|
76
|
+
* change. Idempotent. Returns whether the flag actually CHANGED, mirroring
|
|
77
|
+
* `StarStore.setStarred`. Pass `null` for `username` to write the shared bucket.
|
|
78
|
+
*/
|
|
79
|
+
async setUnread(username, agent, sessionId, unread) {
|
|
80
|
+
const set = await this.ensureLoaded();
|
|
81
|
+
const key = keyOf(username, agent, sessionId);
|
|
82
|
+
if (unread === set.has(key))
|
|
83
|
+
return false; // no-op — avoid a needless write
|
|
84
|
+
if (unread)
|
|
85
|
+
set.add(key);
|
|
86
|
+
else
|
|
87
|
+
set.delete(key);
|
|
88
|
+
await this.persist(set);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Set (or clear) this user's manual-unread flag for MANY chats in ONE write
|
|
93
|
+
* (#508) — the subtree "mark read / mark unread" action. Same single-persist
|
|
94
|
+
* rationale as `ArchiveStore.setManyArchived`: a client-side loop over a
|
|
95
|
+
* 21-chat family can half-succeed and leave the family in two different states.
|
|
96
|
+
* Returns the ids whose flag actually changed.
|
|
97
|
+
*/
|
|
98
|
+
async setManyUnread(username, agent, sessionIds, unread) {
|
|
99
|
+
const set = await this.ensureLoaded();
|
|
100
|
+
const changed = [];
|
|
101
|
+
for (const sessionId of sessionIds) {
|
|
102
|
+
const key = keyOf(username, agent, sessionId);
|
|
103
|
+
if (unread === set.has(key))
|
|
104
|
+
continue; // already in the target state
|
|
105
|
+
if (unread)
|
|
106
|
+
set.add(key);
|
|
107
|
+
else
|
|
108
|
+
set.delete(key);
|
|
109
|
+
changed.push(sessionId);
|
|
110
|
+
}
|
|
111
|
+
if (changed.length)
|
|
112
|
+
await this.persist(set);
|
|
113
|
+
return changed;
|
|
114
|
+
}
|
|
115
|
+
/** Write-through, serialised so overlapping toggles can't corrupt the file. */
|
|
116
|
+
persist(set) {
|
|
117
|
+
this.writing = this.writing.then(async () => {
|
|
118
|
+
const json = JSON.stringify([...set], null, 2);
|
|
119
|
+
await fs.writeFile(this.stateFile, json, { encoding: "utf8", mode: 0o600 }).catch(() => undefined);
|
|
120
|
+
});
|
|
121
|
+
return this.writing;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { TokenTotals } from "./models.js";
|
|
2
|
+
/** A chat's context fill (last turn) plus its cumulative lifetime token totals. */
|
|
3
|
+
export interface SessionTokenUsage {
|
|
4
|
+
/** True once at least one assistant turn carried a usage block. */
|
|
5
|
+
hasData: boolean;
|
|
6
|
+
/** Number of assistant turns (unique message ids) with usage. */
|
|
7
|
+
turnCount: number;
|
|
8
|
+
/**
|
|
9
|
+
* Context-window fill as of the last completed turn:
|
|
10
|
+
* `input + cache_read + cache_creation` of the final assistant message. This
|
|
11
|
+
* matches core's `SessionUsage.inputTokens` so the ring/meter math is unchanged.
|
|
12
|
+
*/
|
|
13
|
+
contextTokens: number;
|
|
14
|
+
/** Cumulative uncached input tokens across every turn. */
|
|
15
|
+
inputTotal: number;
|
|
16
|
+
/** Cumulative output tokens across every turn. */
|
|
17
|
+
outputTotal: number;
|
|
18
|
+
/** Cumulative cache-read input tokens across every turn. */
|
|
19
|
+
cacheReadTotal: number;
|
|
20
|
+
/** Cumulative cache-creation (write) input tokens across every turn. */
|
|
21
|
+
cacheCreationTotal: number;
|
|
22
|
+
/**
|
|
23
|
+
* The same token totals split by the model that produced them (keyed by the
|
|
24
|
+
* `message.model` on each assistant turn). A chat can span models — the
|
|
25
|
+
* composer switches model per turn, and the running model may differ from the
|
|
26
|
+
* project default — so cost must be priced per-model, not at one blended rate.
|
|
27
|
+
* The flat `*Total` fields above stay model-agnostic for the token headline.
|
|
28
|
+
*/
|
|
29
|
+
byModel: Record<string, TokenTotals>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stream a transcript file and accumulate cumulative token usage. Deduplicates
|
|
33
|
+
* assistant messages by `message.id` (keep-first) so repeated stream frames for
|
|
34
|
+
* one message are counted once. Returns {@link EMPTY} for a missing/unreadable
|
|
35
|
+
* file (parity with core, which returns a zeroed summary).
|
|
36
|
+
*/
|
|
37
|
+
export declare function readSessionTokenUsageFile(file: string): Promise<SessionTokenUsage>;
|
|
38
|
+
/**
|
|
39
|
+
* Cumulative token usage for a chat, resolved from `<projectDir>/.chats/`.
|
|
40
|
+
* Memoized on the transcript's mtime. Returns {@link EMPTY} for an unsafe
|
|
41
|
+
* session id or a session with no transcript yet.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readSessionTokenUsage(projectDir: string, sessionId: string): Promise<SessionTokenUsage>;
|
|
44
|
+
/**
|
|
45
|
+
* Per-message context-window fill, keyed by the transcript record `uuid` that the
|
|
46
|
+
* messages DTO carries (issue #451). For every `type:"assistant"` record we store
|
|
47
|
+
* `input + cache_read + cache_creation` — the context-window fill at that turn.
|
|
48
|
+
*
|
|
49
|
+
* A single assistant message is written across several JSONL records (one per
|
|
50
|
+
* content block: text, tool_use, …), each with its own `uuid` but the SAME
|
|
51
|
+
* message-level `usage`, so keying by record `uuid` resolves any of a message's
|
|
52
|
+
* turn ids to the identical fill. Callers forward-fill the value across the
|
|
53
|
+
* non-assistant records between turns (user prompts, tool results) to answer
|
|
54
|
+
* "how full was the context as of this point" for every message.
|
|
55
|
+
*
|
|
56
|
+
* This is a SEPARATE pass from {@link readSessionTokenUsageFile} on purpose: it
|
|
57
|
+
* only runs on the open-chat messages path, never the chat-list rings, so the
|
|
58
|
+
* list DTO stays lean. It rides its own mtime cache below.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readContextSeriesFile(file: string): Promise<Map<string, number>>;
|
|
61
|
+
/**
|
|
62
|
+
* Per-message context series for a chat, resolved from `<projectDir>/.chats/`.
|
|
63
|
+
* Memoized on the transcript's mtime (parity with {@link readSessionTokenUsage}).
|
|
64
|
+
* Returns an empty map for an unsafe session id or a session with no transcript.
|
|
65
|
+
*/
|
|
66
|
+
export declare function readContextSeries(projectDir: string, sessionId: string): Promise<Map<string, number>>;
|