mixdog 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/package.json +8 -8
  2. package/scripts/verify-runtime-dependency-versions.mjs +158 -0
  3. package/src/agents/reviewer/AGENT.md +5 -7
  4. package/src/agents/reviewer/agent.json +1 -1
  5. package/src/defaults/shipped-history.json +4 -0
  6. package/src/defaults/skills/browser-use/SKILL.md +6 -3
  7. package/src/defaults/skills/local-provider/SKILL.md +33 -6
  8. package/src/rules/lead/LEAD.md +5 -8
  9. package/src/rules/shared/45-destructive.md +5 -1
  10. package/src/rules/shared/60-verification.md +6 -4
  11. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
  12. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
  13. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
  16. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  17. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  18. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
  19. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
  21. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
  22. package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
  24. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
  25. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
  26. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
  27. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
  28. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
  29. package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
  30. package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
  31. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
  32. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
  33. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
  34. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
  37. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
  38. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
  39. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
  41. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
  42. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
  43. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
  45. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
  46. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
  47. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
  50. package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
  51. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
  52. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
  53. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
  54. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
  56. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
  57. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
  58. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
  59. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
  60. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
  61. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
  63. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
  64. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
  65. package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
  66. package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
  68. package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
  69. package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
  70. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
  71. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
  72. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
  73. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
  74. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
  75. package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
  76. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
  77. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
  79. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
  81. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
  82. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
  84. package/src/runtime/attachments/legacy-office.mjs +110 -0
  85. package/src/runtime/attachments/prompt-restore.mjs +70 -0
  86. package/src/runtime/attachments/store.mjs +60 -3
  87. package/src/runtime/effort-judge/judge-client.mjs +61 -14
  88. package/src/runtime/effort-judge/model-install.mjs +34 -2
  89. package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
  90. package/src/runtime/office/portable/portable-soffice.mjs +132 -30
  91. package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
  92. package/src/runtime/shared/agent-avatar.mjs +3 -2
  93. package/src/runtime/shared/agent-room-host.d.mts +31 -0
  94. package/src/runtime/shared/agent-room-host.mjs +58 -0
  95. package/src/runtime/shared/bot-hops.mjs +16 -11
  96. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  97. package/src/runtime/shared/redact-directories.mjs +18 -0
  98. package/src/runtime/shared/remove-directory.mjs +28 -0
  99. package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
  100. package/src/runtime/shared/session-sidebar-host.mjs +16 -3
  101. package/src/runtime/shared/tool-execution-contract.mjs +4 -0
  102. package/src/runtime/shared/tool-work-units.mjs +12 -9
  103. package/src/runtime/shared/turn-snapshot.mjs +233 -122
  104. package/src/runtime/shared/user-data-guard.mjs +51 -19
  105. package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
  106. package/src/runtime/shared/worktrees/contract.mjs +11 -3
  107. package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
  108. package/src/runtime/shared/worktrees/manager.mjs +419 -52
  109. package/src/runtime/shared/worktrees/registry.mjs +3 -1
  110. package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
  111. package/src/runtime/tidy/process.mjs +41 -8
  112. package/src/runtime/tidy/resolve.mjs +10 -7
  113. package/src/session-runtime/agent-home-delivery.mjs +73 -91
  114. package/src/session-runtime/agent-profile-changes.mjs +16 -0
  115. package/src/session-runtime/agent-room-delivery.mjs +2 -63
  116. package/src/session-runtime/agent-task-sessions.mjs +194 -64
  117. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  118. package/src/session-runtime/boot/apis.mjs +3 -0
  119. package/src/session-runtime/boot/facade.mjs +5 -6
  120. package/src/session-runtime/boot/tools.mjs +23 -1
  121. package/src/session-runtime/held-requests.mjs +20 -25
  122. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
  123. package/src/session-runtime/model-recency.mjs +1 -0
  124. package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
  125. package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
  126. package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
  127. package/src/session-runtime/runtime-review-api.mjs +4 -1
  128. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  129. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  130. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +44 -20
  131. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  132. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
  133. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  134. package/src/session-runtime/services/provider-admin.mjs +12 -2
  135. package/src/session-runtime/services/worktree-access.mjs +27 -0
  136. package/src/session-runtime/session-hooks.mjs +9 -17
  137. package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
  138. package/src/session-runtime/setup-tool/executor.mjs +4 -0
  139. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
  140. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
  141. package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
  142. package/src/session-runtime/tool-policy-refresh.mjs +1 -0
  143. package/src/session-runtime/tool-surface.mjs +9 -5
  144. package/src/session-runtime/turn/session-ops.mjs +8 -0
  145. package/src/session-runtime/turn/turn-run.mjs +5 -1
  146. package/src/session-runtime/usage-stats-api.mjs +1 -1
  147. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
  148. package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
  149. package/src/session-runtime/workspace/transition-steps.mjs +1 -18
  150. package/src/session-runtime/workspace/transitions.mjs +279 -67
  151. package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
  152. package/src/standalone/daemon-lag-profiler.mjs +3 -1
  153. package/src/standalone/daemon-session-control.mjs +140 -23
  154. package/src/standalone/daemon-telemetry.mjs +9 -0
  155. package/src/standalone/daemon.mjs +5 -0
  156. package/src/standalone/local-session-runtime.mjs +11 -2
  157. package/src/standalone/session-protocol.mjs +1 -0
  158. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
  159. package/src/standalone/session-service/agent-route-follow.mjs +69 -0
  160. package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
  161. package/src/standalone/session-service.mjs +10 -0
  162. package/src/standalone/session-tool-output.mjs +1 -0
  163. package/src/standalone/staged-import.mjs +47 -0
  164. package/src/tui/App.jsx +2 -0
  165. package/src/tui/app/message-selector.mjs +15 -2
  166. package/src/tui/app/model-options.mjs +16 -2
  167. package/src/tui/app/model-picker.mjs +2 -1
  168. package/src/tui/app/route-pickers.mjs +3 -1
  169. package/src/tui/dist/index.mjs +403 -210
  170. package/src/tui/paste-attachments.mjs +0 -1
  171. package/src/tui/paste-text-policy.mjs +4 -5
  172. package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
  173. package/src/tui/session/notice-surface.mjs +3 -0
  174. package/src/tui/session/session-api/intake/abort.mjs +33 -17
  175. package/src/tui/session/session-api/intake/rewind.mjs +39 -4
  176. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  177. package/src/tui/session/tool-approval.mjs +11 -48
  178. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  179. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  180. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  181. package/src/session-runtime/room-member-gate.mjs +0 -61
  182. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -1,78 +1,15 @@
1
1
  /**
2
- * Durable per-day usage rollup (provider × model).
3
- *
4
- * The gateway usage store keeps one raw event per turn, but it is bounded to a
5
- * recent window (TTL + row cap) because it exists to answer "what is happening
6
- * right now". Totals for a month ago cannot be recovered from it once the cap
7
- * evicts those rows, so every turn is ALSO folded into a per-day bucket here.
8
- * A bucket is a few numbers, which keeps years of history in one small file.
9
- *
10
- * Most turns on this machine are NOT the user typing: background runners
11
- * (memory cycles, schedules, webhooks) spend tokens on their own and outnumber
12
- * conversation turns several times over. Every bucket therefore carries a
13
- * `conversation` sub-total beside the full one, so "what did I spend" can be
14
- * answered without the background drowning it.
15
- *
16
- * Session token totals are kept id-keyed only while a day is recent; older days
17
- * keep the anonymous per-session totals alone. Only conversation sessions are
18
- * tracked: a background runner mints a fresh id per cycle, which would turn
19
- * "how many sessions" into a count of cycles.
20
- *
21
- * `firstTs`/`lastTs` bound the turns already folded into a day. A reader that
22
- * also holds the raw event list adds ONLY the events outside that span, which
23
- * is what lets both stores be read together without counting a turn twice.
24
- *
25
- * Shape:
26
- * {
27
- * version, updatedAt,
28
- * days: {
29
- * "2026-08-12": {
30
- * turns, input, output, cacheRead, cacheWrite,
31
- * costUsd, costBilled, costEstimated, costKnownTurns,
32
- * durationMs, durationTurns, firstTs, lastTs,
33
- * conversation: { …the same turn totals, user-driven turns only },
34
- * conversationPartial,
35
- * models: { "<provider>/<model>": {
36
- * provider, model, kind, turns, input, output, cacheRead, cacheWrite,
37
- * costUsd, conversation: { …route totals }
38
- * } },
39
- * sessions: { "<sessionId>": <tokens> },
40
- * sessionTokens: [<tokens>, …],
41
- * sessionsDropped
42
- * }
43
- * }
44
- * }
2
+ * Day keys, conversation-source classification and numeric helpers shared by the
3
+ * usage ledger and the stats model. The daily JSON rollup these helpers were
4
+ * written for is no longer produced; its past day records are still imported
5
+ * by usage-ledger-import.mjs.
45
6
  */
46
- import { billableInputTokensForProvider } from './cost.mjs';
47
-
48
- const USAGE_ROLLUP_VERSION = 2;
49
-
50
- // A day costs well under a kilobyte, so a long retention stays cheap while
51
- // making "all time" mean something closer to the install date.
52
- const RETENTION_DAYS = 400;
53
- // Session ids are the only identifying values in the file; drop them once the
54
- // day is old enough that per-session drill-down no longer matters.
55
- const SESSION_DETAIL_DAYS = 45;
56
- const MAX_MODELS_PER_DAY = 80;
57
- const MAX_SESSION_IDS_PER_DAY = 400;
58
- const MAX_SESSION_TOKENS_PER_DAY = 400;
59
- const DAY_MS = 24 * 60 * 60 * 1000;
60
- const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
61
7
 
62
8
  export function num(value) {
63
9
  const n = Number(value);
64
10
  return Number.isFinite(n) && n > 0 ? n : 0;
65
11
  }
66
12
 
67
- export function cleanId(value) {
68
- const text = typeof value === 'string' ? value.trim() : '';
69
- return text && !UNSAFE_KEYS.has(text) ? text.slice(0, 200) : '';
70
- }
71
-
72
- export function round6(value) {
73
- return Math.round(value * 1e6) / 1e6;
74
- }
75
-
76
13
  /** Local calendar day, so "today" in the UI means the user's today. */
77
14
  export function usageRollupDayKey(ts) {
78
15
  const date = new Date(Number.isFinite(Number(ts)) ? Number(ts) : Date.now());
@@ -92,324 +29,8 @@ export function isConversationUsageSource(sourceType) {
92
29
  return !BACKGROUND_SOURCES.has(id);
93
30
  }
94
31
 
95
- function emptyRouteTotals() {
96
- return {
97
- turns: 0,
98
- input: 0,
99
- output: 0,
100
- cacheRead: 0,
101
- cacheWrite: 0,
102
- costUsd: 0,
103
- costBilled: 0,
104
- costEstimated: 0,
105
- costKnownTurns: 0,
106
- };
107
- }
108
-
109
- function emptyTurnTotals() {
110
- return {
111
- ...emptyRouteTotals(),
112
- durationMs: 0,
113
- durationTurns: 0,
114
- };
115
- }
116
-
117
- function addRouteTotals(target, turn, { priced, billed } = {}) {
118
- target.turns += turn.turns;
119
- target.input += turn.input;
120
- target.output += turn.output;
121
- target.cacheRead += turn.cacheRead;
122
- target.cacheWrite += turn.cacheWrite;
123
- target.costUsd = round6(target.costUsd + turn.costUsd);
124
- if (priced) {
125
- target.costKnownTurns += 1;
126
- if (billed) target.costBilled = round6(target.costBilled + turn.costUsd);
127
- else target.costEstimated = round6(target.costEstimated + turn.costUsd);
128
- }
129
- }
130
-
131
- function addTurnTotals(target, turn, pricing) {
132
- addRouteTotals(target, turn, pricing);
133
- const { durationMs } = pricing;
134
- if (durationMs > 0) {
135
- target.durationMs += durationMs;
136
- target.durationTurns += 1;
137
- }
138
- }
139
-
140
- function emptyModel(provider, model) {
141
- return {
142
- provider,
143
- model,
144
- kind: '',
145
- ...emptyRouteTotals(),
146
- conversation: emptyRouteTotals(),
147
- };
148
- }
149
-
150
- function emptyDay() {
151
- return {
152
- ...emptyTurnTotals(),
153
- firstTs: 0,
154
- lastTs: 0,
155
- conversation: emptyTurnTotals(),
156
- conversationPartial: false,
157
- // Rebuilt from session transcripts rather than recorded turn by turn. The
158
- // flag travels with the bucket so a surface can keep saying so long after
159
- // the transcripts it came from are gone.
160
- restored: false,
161
- // Set once a day's pre-classification session ids have been cleared, which
162
- // is what tells a reader the remaining ids can be trusted.
163
- sessionsPurged: true,
164
- models: {},
165
- sessions: {},
166
- sessionTokens: [],
167
- sessionsDropped: 0,
168
- };
169
- }
170
-
171
- function readRouteTotals(raw, target) {
172
- target.turns = num(raw?.turns);
173
- target.input = num(raw?.input);
174
- target.output = num(raw?.output);
175
- target.cacheRead = num(raw?.cacheRead);
176
- target.cacheWrite = num(raw?.cacheWrite);
177
- target.costUsd = round6(num(raw?.costUsd));
178
- target.costBilled = round6(num(raw?.costBilled));
179
- target.costEstimated = round6(
180
- raw?.costEstimated == null ? Math.max(0, target.costUsd - target.costBilled) : num(raw.costEstimated)
181
- );
182
- if (raw?.costKnownTurns != null) target.costKnownTurns = num(raw.costKnownTurns);
183
- else target.costKnownTurns = target.costUsd > 0 ? target.turns : 0;
184
- return target;
185
- }
186
-
187
32
  /** Turns with a known cost: the recorded count, else every turn when the usage carries any cost at all. */
188
33
  export function knownCostTurns(usage, amount = num) {
189
34
  if (usage.costKnownTurns != null) return amount(usage.costKnownTurns);
190
35
  return amount(usage.costUsd) > 0 ? amount(usage.turns) : 0;
191
36
  }
192
-
193
- function readTurnTotals(raw, target) {
194
- readRouteTotals(raw, target);
195
- target.costEstimated = round6(num(raw?.costEstimated));
196
- target.costKnownTurns = num(raw?.costKnownTurns);
197
- target.durationMs = num(raw?.durationMs);
198
- target.durationTurns = num(raw?.durationTurns);
199
- return target;
200
- }
201
-
202
- function normalizeModel(raw, key) {
203
- const slash = typeof key === 'string' ? key.indexOf('/') : -1;
204
- const bucket = emptyModel(
205
- cleanId(raw?.provider) || (slash > 0 ? key.slice(0, slash) : ''),
206
- cleanId(raw?.model) || (slash > 0 ? key.slice(slash + 1) : '')
207
- );
208
- bucket.kind = cleanId(raw?.kind);
209
- readRouteTotals(raw, bucket);
210
- // A bucket written before turns carried a source has no split to read.
211
- // `null` means unknown, which a reader must not quietly treat as zero.
212
- bucket.conversation =
213
- raw?.conversation && typeof raw.conversation === 'object'
214
- ? readRouteTotals(raw.conversation, emptyRouteTotals())
215
- : null;
216
- return bucket;
217
- }
218
-
219
- function normalizeDay(raw) {
220
- const day = emptyDay();
221
- readTurnTotals(raw, day);
222
- day.firstTs = num(raw?.firstTs);
223
- day.lastTs = num(raw?.lastTs);
224
- day.conversation =
225
- raw?.conversation && typeof raw.conversation === 'object'
226
- ? readTurnTotals(raw.conversation, emptyTurnTotals())
227
- : null;
228
- day.conversationPartial = raw?.conversationPartial === true;
229
- day.restored = raw?.restored === true;
230
- // A day that never held unclassified turns needs no purge; one that did is
231
- // trusted only once it says the old ids were cleared.
232
- day.sessionsPurged = !day.conversationPartial || raw?.sessionsPurged === true;
233
- day.sessionsDropped = num(raw?.sessionsDropped);
234
- const models = raw?.models && typeof raw.models === 'object' ? raw.models : {};
235
- for (const [key, value] of Object.entries(models)) {
236
- const id = cleanId(key);
237
- if (!id) continue;
238
- day.models[id] = normalizeModel(value, id);
239
- }
240
- const sessions = raw?.sessions && typeof raw.sessions === 'object' ? raw.sessions : {};
241
- for (const [key, value] of Object.entries(sessions)) {
242
- const id = cleanId(key);
243
- if (!id) continue;
244
- day.sessions[id] = num(value);
245
- }
246
- if (Array.isArray(raw?.sessionTokens)) {
247
- day.sessionTokens = raw.sessionTokens.map(num).slice(-MAX_SESSION_TOKENS_PER_DAY);
248
- }
249
- return day;
250
- }
251
-
252
- /** Defensive read: a hand-edited or partially written file must not throw. */
253
- export function normalizeUsageRollup(raw) {
254
- const days = {};
255
- const source = raw?.days && typeof raw.days === 'object' ? raw.days : {};
256
- for (const [key, value] of Object.entries(source)) {
257
- if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) continue;
258
- days[key] = normalizeDay(value);
259
- }
260
- return {
261
- version: USAGE_ROLLUP_VERSION,
262
- updatedAt: num(raw?.updatedAt),
263
- days,
264
- };
265
- }
266
-
267
- /** Retention + identifier aging. Both bounds are evaluated against `now`. */
268
- function pruneUsageRollup(rollup, now = Date.now()) {
269
- const dropBefore = usageRollupDayKey(now - RETENTION_DAYS * DAY_MS);
270
- const anonymizeBefore = usageRollupDayKey(now - SESSION_DETAIL_DAYS * DAY_MS);
271
- for (const [key, day] of Object.entries(rollup.days)) {
272
- if (key < dropBefore) {
273
- delete rollup.days[key];
274
- continue;
275
- }
276
- if (key >= anonymizeBefore) continue;
277
- const ids = Object.keys(day.sessions);
278
- if (!ids.length) continue;
279
- const archived = day.sessionTokens.concat(ids.map((id) => day.sessions[id]));
280
- day.sessionTokens = archived.slice(-MAX_SESSION_TOKENS_PER_DAY);
281
- day.sessions = {};
282
- }
283
- return rollup;
284
- }
285
-
286
- /**
287
- * Fold one gateway usage summary into the rollup. Pure: the caller owns the
288
- * document, which is what makes this testable without touching disk.
289
- */
290
- export function foldUsageRollup(current, summary, now = Date.now()) {
291
- const rollup = normalizeUsageRollup(current);
292
- const provider = cleanId(summary?.provider);
293
- const model = cleanId(summary?.model);
294
- // A turn that cannot be attributed to a route would silently inflate every
295
- // total it lands in, so it is dropped rather than bucketed as "unknown".
296
- if (!provider || !model) return pruneUsageRollup(rollup, now);
297
-
298
- const at = Number(summary?.ts ?? summary?.at);
299
- const ts = Number.isFinite(at) && at > 0 ? at : now;
300
- const key = usageRollupDayKey(ts);
301
- const day = rollup.days[key] || emptyDay();
302
-
303
- const cacheRead = num(summary?.cacheReadTokens);
304
- const cacheWrite = num(summary?.cacheWriteTokens);
305
- const turn = {
306
- turns: 1,
307
- // Providers disagree about what `inputTokens` counts: several report the
308
- // whole prompt with the cached part inside it. Stripping it where it is
309
- // already included is what keeps one cache hit from being counted twice.
310
- input: billableInputTokensForProvider(provider, num(summary?.inputTokens), cacheRead, cacheWrite),
311
- output: num(summary?.outputTokens),
312
- cacheRead,
313
- cacheWrite,
314
- costUsd: num(summary?.costUsd),
315
- };
316
- const tokens = turn.input + turn.output + turn.cacheRead + turn.cacheWrite;
317
- const costSource = cleanId(summary?.costSource);
318
- // costSource 'none' means neither the provider nor the catalog priced this
319
- // turn; counting it keeps the surface honest about partial cost coverage.
320
- const pricing = {
321
- priced:
322
- summary?.costUsd != null &&
323
- Number.isFinite(Number(summary.costUsd)) &&
324
- !['', 'none', 'unpriced'].includes(costSource),
325
- billed: costSource === 'provider',
326
- durationMs: num(summary?.durationMs),
327
- };
328
- const conversation = isConversationUsageSource(summary?.sourceType);
329
-
330
- addTurnTotals(day, turn, pricing);
331
- day.firstTs = day.firstTs > 0 ? Math.min(day.firstTs, ts) : ts;
332
- day.lastTs = Math.max(day.lastTs, ts);
333
- if (conversation) {
334
- if (!day.conversation) {
335
- // A day carried over from before turns were classified: what is already
336
- // in it stays unattributed, and the flag lets a reader say so.
337
- day.conversation = emptyTurnTotals();
338
- day.conversationPartial = true;
339
- // Those ids were collected while background cycles still counted, and
340
- // most of them are cycles. Keeping them would report cycles as sessions.
341
- day.sessions = {};
342
- day.sessionTokens = [];
343
- day.sessionsPurged = true;
344
- }
345
- addTurnTotals(day.conversation, turn, pricing);
346
- }
347
-
348
- const modelKey = `${provider}/${model}`;
349
- const known = Object.hasOwn(day.models, modelKey);
350
- if (known || Object.keys(day.models).length < MAX_MODELS_PER_DAY) {
351
- const bucket = known ? day.models[modelKey] : emptyModel(provider, model);
352
- bucket.kind = cleanId(summary?.providerKind) || bucket.kind;
353
- addRouteTotals(bucket, turn, pricing);
354
- if (conversation) {
355
- if (!bucket.conversation) bucket.conversation = emptyRouteTotals();
356
- addRouteTotals(bucket.conversation, turn, pricing);
357
- }
358
- day.models[modelKey] = bucket;
359
- }
360
-
361
- // Only conversation sessions are kept: a background runner mints a new id per
362
- // cycle, so counting those would report cycles as sessions.
363
- const sessionId = conversation ? cleanId(summary?.sessionId) : '';
364
- if (sessionId) {
365
- if (Object.hasOwn(day.sessions, sessionId)) {
366
- day.sessions[sessionId] += tokens;
367
- } else if (Object.keys(day.sessions).length < MAX_SESSION_IDS_PER_DAY) {
368
- day.sessions[sessionId] = tokens;
369
- } else {
370
- day.sessionsDropped += 1;
371
- }
372
- }
373
-
374
- rollup.days[key] = day;
375
- rollup.updatedAt = now;
376
- return pruneUsageRollup(rollup, now);
377
- }
378
-
379
- /**
380
- * Write transcript-rebuilt days into the rollup, so they outlive the sessions
381
- * they were derived from.
382
- *
383
- * The session store prunes: ephemeral sessions past 14 days or beyond the
384
- * newest 300 are deleted, which silently takes their tokens out of any figure
385
- * derived from them. The rollup is a few numbers per day kept for 400 days, so
386
- * once a rebuilt day lands here it stops depending on the transcripts at all.
387
- *
388
- * Recording always wins. A day the rollup already holds is left exactly as it
389
- * is — including a day already frozen this way, because re-deriving it from a
390
- * half-pruned store would quietly shrink it.
391
- *
392
- * Pure: the caller owns the document.
393
- */
394
- export function freezeRestoredDays(current, historyDays, now = Date.now()) {
395
- const rollup = normalizeUsageRollup(current);
396
- const source = historyDays && typeof historyDays === 'object' ? historyDays : {};
397
- const dropBefore = usageRollupDayKey(now - RETENTION_DAYS * DAY_MS);
398
- const today = usageRollupDayKey(now);
399
- let frozen = 0;
400
- for (const [key, day] of Object.entries(source)) {
401
- if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) continue;
402
- if (Object.hasOwn(rollup.days, key)) continue;
403
- if (key < dropBefore) continue;
404
- // Today is still being recorded turn by turn; freezing it now would pin a
405
- // partial rebuild and lock out the rest of the day's real events.
406
- if (key >= today) continue;
407
- const bucket = normalizeDay(day);
408
- if (bucket.turns <= 0) continue;
409
- bucket.restored = true;
410
- rollup.days[key] = bucket;
411
- frozen += 1;
412
- }
413
- if (frozen > 0) rollup.updatedAt = now;
414
- return { rollup, frozen };
415
- }
@@ -0,0 +1,18 @@
1
+ import { pathToFileURL } from 'node:url';
2
+
3
+ /**
4
+ * `text` with every mention of the given directories taken out (and the
5
+ * separator after it), whether written as a native path, with forward slashes
6
+ * or as a file URL, so an error never discloses a private scratch location.
7
+ */
8
+ export function redactDirectories(text, directories) {
9
+ let out = String(text ?? '');
10
+ for (const directory of directories) {
11
+ if (!directory) continue;
12
+ const forms = new Set([directory, directory.replaceAll('\\', '/'), pathToFileURL(directory).href]);
13
+ for (const form of [...forms].sort((a, b) => b.length - a.length)) {
14
+ out = out.split(`${form}\\`).join('').split(`${form}/`).join('').split(form).join('');
15
+ }
16
+ }
17
+ return out;
18
+ }
@@ -0,0 +1,28 @@
1
+ import { rm } from 'node:fs/promises';
2
+
3
+ export const REMOVE_RETRY_MS = 100;
4
+ const REMOVE_ATTEMPTS = 6;
5
+ const RETRYABLE_REMOVE_CODES = new Set(['EBUSY', 'EPERM', 'ENOTEMPTY', 'EACCES']);
6
+
7
+ /**
8
+ * Removes a private scratch folder. A process that just exited can still hold
9
+ * its files for a moment on Windows, so a busy folder is retried briefly; a
10
+ * failure that persists is logged (never swallowed) and does not throw.
11
+ */
12
+ export async function removeDirectoryWithRetry(
13
+ dir,
14
+ { remove = rm, log = (line) => process.stderr.write(line), retryMs = REMOVE_RETRY_MS } = {}
15
+ ) {
16
+ for (let attempt = 1; ; attempt += 1) {
17
+ try {
18
+ await remove(dir, { recursive: true, force: true });
19
+ return;
20
+ } catch (error) {
21
+ if (attempt >= REMOVE_ATTEMPTS || !RETRYABLE_REMOVE_CODES.has(error?.code)) {
22
+ log(`scratch folder ${dir} could not be removed: ${error?.message || error}\n`);
23
+ return;
24
+ }
25
+ await new Promise((resolve) => setTimeout(resolve, retryMs * attempt));
26
+ }
27
+ }
28
+ }
@@ -1,4 +1,13 @@
1
- export type SessionSidebarChange = { favorite: boolean } | { archived: boolean };
1
+ export type SessionSidebarChange =
2
+ | { favorite: boolean }
3
+ | { archived: boolean }
4
+ | { archived: true; cascade: false }
5
+ | { forgotten: true };
2
6
  export type SessionSidebarHost = (sessionId: unknown, change: unknown) => unknown;
3
- export function registerSessionSidebarHost(host: SessionSidebarHost): () => void;
7
+ export type SessionSidebarState = { archived: Record<string, number> };
8
+ export function registerSessionSidebarHost(
9
+ host: SessionSidebarHost,
10
+ options?: { state?: (() => SessionSidebarState | Promise<SessionSidebarState>) | null }
11
+ ): () => void;
4
12
  export function applySessionSidebarChange(sessionId: string, change: SessionSidebarChange): Promise<void>;
13
+ export function readSessionSidebarState(): Promise<SessionSidebarState | null>;
@@ -15,11 +15,15 @@ function hosts() {
15
15
 
16
16
  /**
17
17
  * Register a desktop host. `host(sessionId, change)` applies one change:
18
- * `{ favorite: boolean }` or `{ archived: boolean }`.
18
+ * `{ favorite: boolean }`, `{ archived: boolean }` (an archive takes the
19
+ * session's sub-agent sessions with it, a restore brings back the ones it took),
20
+ * `{ archived: true, cascade: false }` (that session alone)
21
+ * or `{ forgotten: true }` (a session the store deleted: its records go).
22
+ * `state()` answers the archive flags: `{ archived }`, session id → stamp.
19
23
  * @returns {() => void} Unregister.
20
24
  */
21
- export function registerSessionSidebarHost(host) {
22
- const entry = { host };
25
+ export function registerSessionSidebarHost(host, { state = null } = {}) {
26
+ const entry = { host, state: typeof state === 'function' ? state : null };
23
27
  hosts().push(entry);
24
28
  return () => {
25
29
  const list = hosts();
@@ -36,3 +40,12 @@ export async function applySessionSidebarChange(sessionId, change) {
36
40
  if (!active) throw new Error('no desktop app is attached to this runtime');
37
41
  await active.host(sessionId, change);
38
42
  }
43
+
44
+ /** The active desktop's flags, `{ archived }` (session id → epoch-ms stamp);
45
+ * null when no desktop (or one without a state reader) is attached. */
46
+ export async function readSessionSidebarState() {
47
+ const active = hosts().at(-1);
48
+ if (!active?.state) return null;
49
+ const state = await active.state();
50
+ return { archived: { ...state?.archived } };
51
+ }
@@ -1,4 +1,5 @@
1
1
  import { clean } from './clean.mjs';
2
+ import { hopOf } from './bot-hops.mjs';
2
3
  import {
3
4
  parseTaskNotification,
4
5
  renderTaskCompletionEnvelope,
@@ -113,6 +114,8 @@ export function normalizeToolNotifyContext(context = {}) {
113
114
  callerSessionId: callerSessionId || null,
114
115
  routingSessionId: routingSessionId || null,
115
116
  clientHostPid,
117
+ // The bot-chain hop of the turn that launched the work (bot-hops.mjs).
118
+ ...(hopOf(context.hop) ? { hop: context.hop } : {}),
116
119
  };
117
120
  }
118
121
 
@@ -133,6 +136,7 @@ function toolCompletionMeta({ surface = 'tool', id, status, resultType, instruct
133
136
  ? { routing_session_id: ctx.routingSessionId }
134
137
  : {}),
135
138
  ...(ctx.clientHostPid ? { client_host_pid: String(ctx.clientHostPid) } : {}),
139
+ ...(ctx.hop ? { hop: ctx.hop } : {}),
136
140
  };
137
141
  }
138
142
 
@@ -573,6 +573,15 @@ function gitStageUnit(a) {
573
573
  });
574
574
  }
575
575
 
576
+ function gitWorktreeUnit(a) {
577
+ return unitDescriptor('Git', {
578
+ active: 'Managing',
579
+ done: 'Managed',
580
+ noun: 'worktree',
581
+ effect: String(a.worktree_action || '').toLowerCase() !== 'list',
582
+ });
583
+ }
584
+
576
585
  function cwdUnit(a) {
577
586
  const action = String(a.action || a.type || '').toLowerCase();
578
587
  // A bare `path` selects that Project even when action is omitted.
@@ -604,14 +613,6 @@ function cwdUnit(a) {
604
613
  const [active, done] = words;
605
614
  return unitDescriptor('Setup', { active, done, noun: 'session', effect: true });
606
615
  }
607
- if (action.endsWith('-worktree') || action.endsWith('-worktrees')) {
608
- return unitDescriptor('Setup', {
609
- active: 'Managing',
610
- done: 'Managed',
611
- noun: 'worktree',
612
- effect: !action.startsWith('list'),
613
- });
614
- }
615
616
  if (action === 'status') {
616
617
  return unitDescriptor('Setup', { active: 'Checking', done: 'Checked', noun: 'session' });
617
618
  }
@@ -715,7 +716,9 @@ const TOOL_UNITS = new Map([
715
716
  (a) =>
716
717
  a.action === 'stage'
717
718
  ? gitStageUnit(a)
718
- : unitDescriptor('Git', { count: queryCount(a, 'command', 'commands') || 1, noun: 'Git command' }),
719
+ : a.action === 'worktree'
720
+ ? gitWorktreeUnit(a)
721
+ : unitDescriptor('Git', { count: queryCount(a, 'command', 'commands') || 1, noun: 'Git command' }),
719
722
  ],
720
723
  ['github', githubUnit],
721
724
  // Preserve the staging work unit when rendering historical transcripts.