@letrquan/book 0.3.0 → 0.4.0-beta.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
package/dist/sdk.d.ts CHANGED
@@ -56,6 +56,7 @@ interface Clock {
56
56
  declare const providerModelSchema: z.ZodObject<{
57
57
  label: z.ZodOptional<z.ZodString>;
58
58
  contextWindow: z.ZodOptional<z.ZodNumber>;
59
+ reportedContextWindow: z.ZodOptional<z.ZodNumber>;
59
60
  maxOutputTokens: z.ZodOptional<z.ZodNumber>;
60
61
  vision: z.ZodOptional<z.ZodBoolean>;
61
62
  manual: z.ZodOptional<z.ZodBoolean>;
@@ -81,6 +82,44 @@ declare const providerModelSchema: z.ZodObject<{
81
82
  }>>>;
82
83
  }, z.core.$strip>]>>;
83
84
  }, z.core.$strip>;
85
+ declare const providerConfigSchema: z.ZodObject<{
86
+ type: z.ZodDefault<z.ZodEnum<{
87
+ openai: "openai";
88
+ anthropic: "anthropic";
89
+ }>>;
90
+ baseURL: z.ZodOptional<z.ZodString>;
91
+ baseUrl: z.ZodOptional<z.ZodString>;
92
+ apiKey: z.ZodOptional<z.ZodString>;
93
+ models: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
94
+ label: z.ZodOptional<z.ZodString>;
95
+ contextWindow: z.ZodOptional<z.ZodNumber>;
96
+ reportedContextWindow: z.ZodOptional<z.ZodNumber>;
97
+ maxOutputTokens: z.ZodOptional<z.ZodNumber>;
98
+ vision: z.ZodOptional<z.ZodBoolean>;
99
+ manual: z.ZodOptional<z.ZodBoolean>;
100
+ editFormat: z.ZodOptional<z.ZodEnum<{
101
+ patch: "patch";
102
+ replace: "replace";
103
+ whole: "whole";
104
+ }>>;
105
+ effort: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
106
+ default: z.ZodOptional<z.ZodEnum<{
107
+ low: "low";
108
+ medium: "medium";
109
+ high: "high";
110
+ xhigh: "xhigh";
111
+ max: "max";
112
+ }>>;
113
+ levels: z.ZodOptional<z.ZodArray<z.ZodEnum<{
114
+ low: "low";
115
+ medium: "medium";
116
+ high: "high";
117
+ xhigh: "xhigh";
118
+ max: "max";
119
+ }>>>;
120
+ }, z.core.$strip>]>>;
121
+ }, z.core.$strip>>>;
122
+ }, z.core.$strip>;
84
123
  declare const toolDiscoverySettingsSchema: z.ZodObject<{
85
124
  mode: z.ZodDefault<z.ZodEnum<{
86
125
  auto: "auto";
@@ -122,6 +161,7 @@ declare const skillSettingsSchema: z.ZodObject<{
122
161
  }, z.core.$strip>;
123
162
  type SkillSettings = z.infer<typeof skillSettingsSchema>;
124
163
  type ProviderModelConfig = z.infer<typeof providerModelSchema>;
164
+ type ProviderConfig = z.infer<typeof providerConfigSchema>;
125
165
  /** The supported production strategy. */
126
166
  declare const compactStrategySchema: z.ZodLiteral<"summary">;
127
167
  type CompactStrategy = z.infer<typeof compactStrategySchema>;
@@ -140,6 +180,7 @@ declare const bookSettingsSchema: z.ZodObject<{
140
180
  xhigh: "xhigh";
141
181
  max: "max";
142
182
  }>>;
183
+ advisorModel: z.ZodOptional<z.ZodString>;
143
184
  maxTurns: z.ZodOptional<z.ZodNumber>;
144
185
  maxTokens: z.ZodOptional<z.ZodNumber>;
145
186
  effort: z.ZodOptional<z.ZodEnum<{
@@ -184,6 +225,13 @@ declare const bookSettingsSchema: z.ZodObject<{
184
225
  approved: "approved";
185
226
  rejected: "rejected";
186
227
  }>>>;
228
+ workspaceProviders: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
229
+ fingerprint: z.ZodString;
230
+ choice: z.ZodEnum<{
231
+ approved: "approved";
232
+ rejected: "rejected";
233
+ }>;
234
+ }, z.core.$strip>>>;
187
235
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
188
236
  provider: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
189
237
  type: z.ZodDefault<z.ZodEnum<{
@@ -196,6 +244,7 @@ declare const bookSettingsSchema: z.ZodObject<{
196
244
  models: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
197
245
  label: z.ZodOptional<z.ZodString>;
198
246
  contextWindow: z.ZodOptional<z.ZodNumber>;
247
+ reportedContextWindow: z.ZodOptional<z.ZodNumber>;
199
248
  maxOutputTokens: z.ZodOptional<z.ZodNumber>;
200
249
  vision: z.ZodOptional<z.ZodBoolean>;
201
250
  manual: z.ZodOptional<z.ZodBoolean>;
@@ -241,6 +290,7 @@ declare const bookSettingsSchema: z.ZodObject<{
241
290
  allowWrite: z.ZodDefault<z.ZodArray<z.ZodString>>;
242
291
  denyWrite: z.ZodDefault<z.ZodArray<z.ZodString>>;
243
292
  denyRead: z.ZodDefault<z.ZodArray<z.ZodString>>;
293
+ allowGitWrites: z.ZodDefault<z.ZodBoolean>;
244
294
  }, z.core.$strip>>;
245
295
  network: z.ZodPrefault<z.ZodObject<{
246
296
  allowedDomains: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -316,6 +366,7 @@ declare const bookSettingsSchema: z.ZodObject<{
316
366
  requestTimeoutMs: z.ZodDefault<z.ZodNumber>;
317
367
  streamStallTimeoutMs: z.ZodDefault<z.ZodNumber>;
318
368
  thinkingStallTimeoutMs: z.ZodDefault<z.ZodNumber>;
369
+ firstDeltaStallTimeoutMs: z.ZodDefault<z.ZodNumber>;
319
370
  toolRetries: z.ZodDefault<z.ZodNumber>;
320
371
  watchdog: z.ZodDefault<z.ZodBoolean>;
321
372
  streamReissueAttempts: z.ZodDefault<z.ZodNumber>;
@@ -324,15 +375,6 @@ declare const bookSettingsSchema: z.ZodObject<{
324
375
  memory: z.ZodPrefault<z.ZodObject<{
325
376
  enabled: z.ZodDefault<z.ZodBoolean>;
326
377
  autoSave: z.ZodDefault<z.ZodBoolean>;
327
- requireApproval: z.ZodDefault<z.ZodBoolean>;
328
- quarantineExternal: z.ZodDefault<z.ZodBoolean>;
329
- extraction: z.ZodPrefault<z.ZodObject<{
330
- enabled: z.ZodDefault<z.ZodBoolean>;
331
- idleHours: z.ZodDefault<z.ZodNumber>;
332
- minMessages: z.ZodDefault<z.ZodNumber>;
333
- maxPerSession: z.ZodDefault<z.ZodNumber>;
334
- maxSessionsPerRun: z.ZodDefault<z.ZodNumber>;
335
- }, z.core.$strip>>;
336
378
  }, z.core.$strip>>;
337
379
  continuation: z.ZodPrefault<z.ZodObject<{
338
380
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -345,8 +387,8 @@ declare const bookSettingsSchema: z.ZodObject<{
345
387
  agents: z.ZodPrefault<z.ZodObject<{
346
388
  mode: z.ZodDefault<z.ZodEnum<{
347
389
  manual: "manual";
348
- adaptive: "adaptive";
349
390
  off: "off";
391
+ adaptive: "adaptive";
350
392
  }>>;
351
393
  maxConcurrent: z.ZodDefault<z.ZodNumber>;
352
394
  maxSpawned: z.ZodDefault<z.ZodNumber>;
@@ -423,31 +465,43 @@ type BookSettings = z.infer<typeof bookSettingsSchema>;
423
465
  /**
424
466
  * Resolved settings after layering (all defaults filled).
425
467
  */
426
- type ResolvedSettings = Required<Omit<BookSettings, 'model' | 'compactModel' | 'compactEffort' | 'maxTurns' | 'maxTokens' | 'effort' | 'theme' | 'shell' | 'autoCompactEnabled' | 'defaultMode' | 'disableBypassPermissionsMode'>> & Pick<BookSettings, 'model' | 'compactModel' | 'compactEffort' | 'maxTurns' | 'maxTokens' | 'effort' | 'theme' | 'shell' | 'autoCompactEnabled' | 'defaultMode' | 'disableBypassPermissionsMode'>;
468
+ type ResolvedSettings = Required<Omit<BookSettings, 'model' | 'compactModel' | 'compactEffort' | 'advisorModel' | 'maxTurns' | 'maxTokens' | 'effort' | 'theme' | 'shell' | 'autoCompactEnabled' | 'defaultMode' | 'disableBypassPermissionsMode'>> & Pick<BookSettings, 'model' | 'compactModel' | 'compactEffort' | 'advisorModel' | 'maxTurns' | 'maxTokens' | 'effort' | 'theme' | 'shell' | 'autoCompactEnabled' | 'defaultMode' | 'disableBypassPermissionsMode'>;
427
469
 
428
470
  declare const MEMORY_TYPES: readonly ["user", "feedback", "project", "reference"];
429
471
  type MemoryType = (typeof MEMORY_TYPES)[number];
430
- type MemoryStatus = 'approved' | 'pending' | 'discarded' | 'superseded';
431
472
  interface MemoryFileSummary {
432
473
  name: string;
433
474
  path: string;
434
475
  type?: MemoryType;
435
- status?: MemoryStatus;
436
476
  title?: string;
437
- created?: string;
438
- updated?: string;
477
+ /** The frontmatter `description`: what the memory is for, in a line. */
478
+ description?: string;
439
479
  size: number;
440
480
  driver?: SessionDriver;
481
+ /**
482
+ * The file still carries the retired `status: superseded` frontmatter of the old save tool. It
483
+ * is no longer retired by anything, so it loads like any other file; `/memory status` lists it
484
+ * as legacy.
485
+ */
486
+ legacySuperseded?: boolean;
441
487
  }
442
488
  interface LoadedMemoryContext {
443
489
  dir: string;
444
490
  indexFile: string | null;
445
491
  indexLoaded: boolean;
492
+ /** Non-empty lines in MEMORY.md (the whole file, not only what loaded). */
446
493
  indexLineCount: number;
494
+ /** Non-empty lines that loaded, the cut marker excluded. */
447
495
  loadedLineCount: number;
496
+ /** UTF-8 size of the trimmed MEMORY.md. */
497
+ indexByteCount: number;
498
+ /** The index was cut at the line limit. */
499
+ wasLineTruncated: boolean;
500
+ /** The index was cut at the byte limit. */
501
+ wasByteTruncated: boolean;
502
+ /** The loaded index; ends with a warning marker when it was cut. */
448
503
  indexText: string;
449
504
  files: MemoryFileSummary[];
450
- candidates: MemoryFileSummary[];
451
505
  }
452
506
 
453
507
  interface LearnedModelWindowEntry {
@@ -461,6 +515,89 @@ interface ModelWindowStore {
461
515
  all(): Record<string, LearnedModelWindowEntry>;
462
516
  }
463
517
 
518
+ /** A provider entry as a settings file wrote it: every key optional, nothing defaulted. */
519
+ type ProviderDeclaration = Partial<ProviderConfig>;
520
+ type WorkspaceProviderState = 'approved' | 'rejected' | 'pending';
521
+ /** Override keys one workspace layer wrote for a provider a trusted layer defines. */
522
+ interface DroppedProviderOverride {
523
+ path: string;
524
+ providerId: string;
525
+ /** Which of `baseURL`, `baseUrl`, `apiKey` and `type` the layer wrote. */
526
+ keys: string[];
527
+ }
528
+ /** A provider only workspace layers declare, merged across them, with what the user decides on. */
529
+ interface WorkspaceProviderEntry {
530
+ id: string;
531
+ /** The merged declaration, later layers over earlier ones. */
532
+ declaration: ProviderDeclaration;
533
+ fingerprint: string;
534
+ /** Every layer that declared it, in merge order. */
535
+ layers: string[];
536
+ }
537
+ interface WorkspaceProviderPartition {
538
+ /** The user approved this exact declaration: it joins the resolved providers. */
539
+ approved: WorkspaceProviderEntry[];
540
+ /** No decision recorded for this declaration: withheld until the user makes one. */
541
+ pending: WorkspaceProviderEntry[];
542
+ /** Explicitly refused: withheld, and not re-offered. */
543
+ rejected: WorkspaceProviderEntry[];
544
+ /**
545
+ * A provider named {@link LEGACY_BASE_URL_ID}: that id is where the decision about a legacy
546
+ * `baseUrl` lives, so a decision recorded under it could not tell the two apart. Withheld
547
+ * whatever the trust store holds, and never offered for approval.
548
+ */
549
+ reserved: WorkspaceProviderEntry[];
550
+ }
551
+ /** A model selection a workspace layer made that names a withheld workspace provider. */
552
+ interface WithheldModelSelection {
553
+ /** The slot: `model`, `compactModel`, `advisorModel` or `agents.profiles.<name>.model`. */
554
+ key: string;
555
+ value: string;
556
+ providerId: string;
557
+ /** The workspace layer the value came from. */
558
+ path: string;
559
+ /** What applies instead: the trusted layers' own value, or `undefined` when they set none. */
560
+ fallback: string | undefined;
561
+ }
562
+ /**
563
+ * A model selection the user's own settings, `--model` or the environment made, whose prefix
564
+ * names only a withheld workspace provider. It is unaffected: it resolves as it would had the
565
+ * workspace not declared that provider. Approving the provider is what would change that.
566
+ */
567
+ interface UnaffectedModelSelection {
568
+ key: string;
569
+ value: string;
570
+ providerId: string;
571
+ /** Where it came from: a settings path, `--model` or an environment variable. */
572
+ source: string;
573
+ }
574
+ /** The legacy `.bookrc.json` `baseUrl`, and whether the user approved it. */
575
+ interface LegacyBaseUrlGate {
576
+ baseUrl: string;
577
+ state: WorkspaceProviderState;
578
+ }
579
+ /**
580
+ * What the provider gate did to one resolution (#418), for the config that carries it and for the
581
+ * hosts that report it. Computed from the layer files themselves, never read back from a settings
582
+ * key a workspace layer could write. Build one with {@link createWorkspaceProviderGate}.
583
+ */
584
+ interface WorkspaceProviderGate {
585
+ /** Provider ids only workspace layers define: they never inherit a key. */
586
+ declared: string[];
587
+ /** The subset of {@link declared} that is withheld (pending, rejected or reserved). */
588
+ withheld: string[];
589
+ /** The same ids sorted by the user's decision on the declaration as written. */
590
+ partition: WorkspaceProviderPartition;
591
+ /** Origin, key and protocol overrides a workspace layer wrote for a trusted provider; dropped. */
592
+ droppedOverrides: DroppedProviderOverride[];
593
+ /** Model selections that fell back to the trusted layers' value. */
594
+ withheldSelections: WithheldModelSelection[];
595
+ /** The user's own selections that name only a withheld provider, and so are unaffected. */
596
+ unaffectedSelections: UnaffectedModelSelection[];
597
+ /** The legacy `.bookrc.json` `baseUrl`, when the workspace has one. */
598
+ legacyBaseUrl?: LegacyBaseUrlGate;
599
+ }
600
+
464
601
  type PermissionMode = 'default' | 'auto' | 'plan' | 'accept-edits' | 'dontAsk' | 'bypassPermissions';
465
602
  /**
466
603
  * A command resolved to a direct argv spawn — no intermediate shell parses it.
@@ -472,10 +609,16 @@ type PermissionMode = 'default' | 'auto' | 'plan' | 'accept-edits' | 'dontAsk' |
472
609
  * command string goes to the platform shell as before. Unsandboxed commands
473
610
  * use the same form when the session shell is spawned as argv (see
474
611
  * `ResolvedShell`).
612
+ *
613
+ * `env` holds variables the spawn must add to the child's environment, merged
614
+ * last over everything else. Git Bash and a POSIX-named sh on Windows receive
615
+ * their command there rather than in `args` (see `shellExecution`), so every
616
+ * site that spawns an `exec` must apply it to that spawn alone.
475
617
  */
476
618
  interface CommandExecution {
477
619
  file: string;
478
620
  args: string[];
621
+ env?: Record<string, string>;
479
622
  }
480
623
  /** The program that interprets a Bash tool command string. */
481
624
  type ShellKind = 'bash' | 'pwsh' | 'powershell' | 'cmd' | 'sh';
@@ -498,6 +641,17 @@ interface ResolvedShell {
498
641
  source: ShellSource;
499
642
  /** Set when a requested shell could not be honoured and the automatic ladder was used instead. */
500
643
  warning?: string;
644
+ /**
645
+ * Set on Windows for a `kind: 'bash'` or `kind: 'sh'` executable that runs on the MSYS runtime
646
+ * (Git for Windows' `bin\bash.exe`, `usr\bin\bash.exe` and `sh.exe`; MSYS2's `bash.exe`,
647
+ * `dash.exe`, `zsh.exe` and other POSIX shells), detected at resolution by `msys-2.0.dll` beside
648
+ * it, or in `..\usr\bin` for one in a `bin` directory (#532, #548). It does not decide delivery: every non-WSL bash and every
649
+ * POSIX-named sh gets its command out of band, flagged or not (`receivesCommandOutOfBand`). It
650
+ * only lets the permission scanner read `'` as on Linux (`readsSingleQuoteAsPosix`), because
651
+ * Windows CI proves this runtime's delivery. Never set off Windows, in a WSL launcher directory,
652
+ * or for a non-POSIX shell, a Cygwin shell or a shim or copy with no DLL beside it.
653
+ */
654
+ msys?: true;
501
655
  }
502
656
  /**
503
657
  * What phase of retry is currently active (drives the TUI spinner label).
@@ -538,6 +692,16 @@ interface RetryConfig {
538
692
  outputCapContinuations?: number;
539
693
  /** Stall ceiling while the model is thinking; absent falls back to the chat one. */
540
694
  thinkingStallTimeoutMs?: number;
695
+ /**
696
+ * First-delta ceiling for a model that may think silently (#379): how long a
697
+ * stream may stay quiet before its first content, tool-call or reasoning
698
+ * delta when the request sends no `reasoning_effort` and the model's catalog
699
+ * entry does not settle the question. A router that buffers the whole
700
+ * thinking block sends a role-only chunk and then nothing for up to about
701
+ * 80 s, past the chat ceiling. Optional, and absent falls back to the chat
702
+ * one, so a hand-built RetryConfig keeps the behavior it was written for.
703
+ */
704
+ firstDeltaStallTimeoutMs?: number;
541
705
  }
542
706
  type AgentTaskStatus = 'pending' | 'in_progress' | 'completed' | 'deleted';
543
707
  interface AgentTask {
@@ -592,6 +756,8 @@ interface BackgroundShellRecord {
592
756
  timer?: NodeJS.Timeout;
593
757
  retentionTimer?: NodeJS.Timeout;
594
758
  sandboxed?: boolean;
759
+ /** The sandbox's git-dir mode (issue 373): true = every git dir of the workspace read-only. */
760
+ gitDirReadOnly?: boolean;
595
761
  }
596
762
  interface BackgroundShellStore {
597
763
  nextId: number;
@@ -619,6 +785,8 @@ interface AgentConfig {
619
785
  modelProviderWarning?: string;
620
786
  /** Optional provider/model used only for historical conversation compaction. */
621
787
  compactModel?: string;
788
+ /** Optional stronger provider/model the `Advisor` tool consults (`/advisor`). */
789
+ advisorModel?: string;
622
790
  compactEffort?: AgentConfig['effort'];
623
791
  /** Supported production context-reduction strategy. */
624
792
  compactStrategy: CompactStrategy;
@@ -648,6 +816,24 @@ interface AgentConfig {
648
816
  /** Plain-model fallback values used after provider/model switches. */
649
817
  defaultApiKey?: string;
650
818
  defaultBaseUrl?: string;
819
+ /**
820
+ * Whether `defaultBaseUrl` was chosen with BOOK_BASE_URL rather than being the built-in OpenAI
821
+ * default or a legacy `.bookrc.json` `baseUrl` (a repository can ship that file, so it is no
822
+ * proof of the user's choice). Recorded at load, never inferred from the URL.
823
+ */
824
+ defaultBaseUrlExplicit?: boolean;
825
+ /**
826
+ * Provider ids only a workspace layer declares, computed by the loader from the layer files
827
+ * themselves (#418) -- never from a settings key, which a workspace layer could write. A
828
+ * declared provider never inherits a key, `BOOK_API_KEY` and the session key included; and a
829
+ * withheld one (pending, rejected or reserved) is absent from `settings.provider`, so a model id
830
+ * a workspace controls that names it is refused instead of falling through to the default
831
+ * endpoint, while the user's own selection with that prefix resolves as the plain fallback. The
832
+ * rest of the gate (what was dropped, which selections fell back or were left alone, the legacy
833
+ * `baseUrl`) is what the hosts report. It describes one resolution of the settings: whoever
834
+ * re-resolves them in a session replaces the gate with the one computed alongside.
835
+ */
836
+ workspaceProviderGate?: WorkspaceProviderGate;
651
837
  defaultProvider?: 'anthropic' | 'openai' | 'auto';
652
838
  autoCompactEnabled: boolean;
653
839
  /**
@@ -715,16 +901,29 @@ interface ShellStartOptions {
715
901
  effectiveCommand: string;
716
902
  /**
717
903
  * Present for sandboxed commands and for a session shell spawned as argv
718
- * (Git Bash or PowerShell on Windows); absent means Node's `shell: true`.
904
+ * (on Windows Git Bash, a POSIX sh or PowerShell, where Git Bash and a POSIX
905
+ * sh carry the command in `env`); absent means Node's `shell: true`.
719
906
  */
720
907
  exec?: CommandExecution;
721
908
  workdir: string;
722
909
  env: NodeJS.ProcessEnv;
723
910
  sandboxed: boolean;
911
+ /**
912
+ * The sandbox's git-dir mode (issue 373): true binds every git directory of the
913
+ * workspace read-only, which is what the failed `git` command's note describes.
914
+ * Persisted with the spec so a restored job still reports its own note.
915
+ */
916
+ gitDirReadOnly?: boolean;
724
917
  title?: string;
725
918
  notify?: BackgroundShellNotify;
726
919
  timeoutMs?: number;
727
920
  lifetime?: 'session' | 'persistent';
921
+ /**
922
+ * Ends a persistent start that is still waiting for its runner: the runner is killed and the job
923
+ * forgotten, as for a start that outran its budget. Read only by a persistent start; a session
924
+ * shell is spawned at once and has nothing to wait for.
925
+ */
926
+ signal?: AbortSignal;
728
927
  workspace?: string;
729
928
  envOverrides?: Record<string, string>;
730
929
  parentSessionId?: string;
@@ -745,6 +944,7 @@ interface ShellAdoptOptions {
745
944
  effectiveCommand: string;
746
945
  workdir: string;
747
946
  sandboxed: boolean;
947
+ gitDirReadOnly?: boolean;
748
948
  title?: string;
749
949
  /** When the command really started, not when it was adopted. */
750
950
  startedAt?: number;
@@ -790,8 +990,12 @@ declare class ShellJobManager {
790
990
  private readonly unwrittenLost;
791
991
  /** Set by `dispose()`: a disposed manager takes on no new work. */
792
992
  private disposed;
993
+ /** Secrets the buffer trim must not cut (#411); the Bash tools hand over the session's set. */
994
+ private providerSecrets;
793
995
  constructor(store: BackgroundShellStore, options?: ShellJobManagerOptions);
794
996
  private readonly clock;
997
+ /** The provider secrets the 5 MB buffer trim keeps whole (#411). */
998
+ setProviderSecrets(secrets: readonly string[] | undefined): void;
795
999
  subscribe(listener: (event: ShellJobEvent) => void): () => void;
796
1000
  configureWorkspace(workspace: string): void;
797
1001
  list(): BackgroundShellRecord[];
@@ -821,8 +1025,30 @@ declare class ShellJobManager {
821
1025
  * between.
822
1026
  */
823
1027
  adopt(options: ShellAdoptOptions): BackgroundShellRecord | undefined;
824
- readOutput(shellId: string, limit?: number): ShellOutputResult | undefined;
825
- readTail(shellId: string, limit?: number): string | undefined;
1028
+ /**
1029
+ * The next unread chunk. `secrets` are values the caller will redact from what it gets back
1030
+ * (#411): the chunk never ends inside one, so a key cut in half across two reads cannot slip past
1031
+ * an exact-match redactor. `readOffset` and `remaining` follow the adjusted cut.
1032
+ */
1033
+ readOutput(shellId: string, limit?: number, secrets?: readonly string[]): ShellOutputResult | undefined;
1034
+ /**
1035
+ * Cut the next chunk out of `unread` and advance the cursor past it.
1036
+ *
1037
+ * With secrets configured, a running job's read stops after the last line break, so a key being
1038
+ * written is not returned in halves: the unterminated trailing line waits until it ends or the
1039
+ * job does. The rule looks at line breaks and length only, never at the secrets' values: a
1040
+ * rule that held back "text that starts a key" would let a command probe the key by printing
1041
+ * candidates and seeing which BashOutput comes back empty. A line with no break at all is
1042
+ * returned in {@link UNTERMINATED_READ_CAP}-character pieces so a prompt or a long line is not
1043
+ * held forever. The chunk never ends inside a whole key (`secretSafeCut`), and a finished job
1044
+ * returns everything.
1045
+ */
1046
+ private takeUnread;
1047
+ /**
1048
+ * The last `limit` characters of the output. With `secrets`, a window whose front edge falls
1049
+ * inside a key starts where that key starts, so the redactor the caller applies sees it whole.
1050
+ */
1051
+ readTail(shellId: string, limit?: number, secrets?: readonly string[]): string | undefined;
826
1052
  stop(shellId: string, reason?: 'killed' | 'timed_out'): Promise<boolean>;
827
1053
  dismiss(shellId: string): void;
828
1054
  acknowledgeCompletion(shellId: string, sequence?: number, consumer?: 'ui' | 'agent'): void;
@@ -884,7 +1110,22 @@ declare class ShellJobManager {
884
1110
  }
885
1111
 
886
1112
  type AgentTerminalStatus = 'completed' | 'failed' | 'cancelled' | 'timed_out' | 'interrupted';
887
- type AgentTerminalReason = 'normal_completion' | 'blocked_by_policy' | 'provider_error' | 'provider_timeout' | 'stream_stall' | 'protocol_error' | 'context_overflow' | 'budget_exceeded' | 'budget_unverifiable' | 'max_turns' | 'runtime_error' | 'caller_cancelled' | 'user_cancelled' | 'session_disposed' | 'session_replaced' | 'transport_interrupted' | 'output_cap' | 'credentials_rejected' | 'objective_complete' | 'continuation_limit' | 'blocked_plan' | 'no_progress' | 'all_tools_blocked' | 'plan_stop' | 'handoff_requested' | 'missing_terminal';
1113
+ type AgentTerminalReason = 'normal_completion' | 'blocked_by_policy' | 'provider_error' | 'provider_timeout' | 'stream_stall' | 'protocol_error' | 'context_overflow' | 'budget_exceeded' | 'budget_unverifiable' | 'max_turns' | 'runtime_error' | 'caller_cancelled' | 'user_cancelled' | 'session_disposed' | 'session_replaced' | 'transport_interrupted' | 'output_cap' | 'credentials_rejected'
1114
+ /**
1115
+ * A usage or plan limit is spent, or the provider stated a wait longer than the
1116
+ * retry layer can sit out (provider codes `usage_limit` and `rate_limit_wait`).
1117
+ * Not a rejected key: nothing needs rotating, the run waits for the reset or an
1118
+ * operator picks another model. A 402 and `insufficient_quota` stay
1119
+ * `credentials_rejected`.
1120
+ */
1121
+ | 'quota_exhausted' | 'model_unavailable' | 'objective_complete' | 'continuation_limit' | 'blocked_plan' | 'no_progress'
1122
+ /** The run's `/goal` was judged impossible, or was still unmet at the check cap. */
1123
+ | 'goal_unmet'
1124
+ /**
1125
+ * The run's `/goal` check could not run (provider error, timeout, unreadable
1126
+ * answer). The run was let end rather than trapped, but nothing confirmed the goal.
1127
+ */
1128
+ | 'goal_unverified' | 'all_tools_blocked' | 'plan_stop' | 'handoff_requested' | 'missing_terminal';
888
1129
  interface AgentTerminalOutcome {
889
1130
  readonly status: AgentTerminalStatus;
890
1131
  readonly reason: AgentTerminalReason;
@@ -893,6 +1134,52 @@ interface AgentTerminalOutcome {
893
1134
  readonly providerCode?: string;
894
1135
  }
895
1136
 
1137
+ /**
1138
+ * A session goal set with `/goal <condition>`: the host keeps the agent working
1139
+ * until a separate evaluator judges, from the transcript, that the condition
1140
+ * holds. Lives on the session runtime and survives a resume through `goal`
1141
+ * session records.
1142
+ */
1143
+ interface ActiveGoal {
1144
+ /** The condition as the user wrote it; also the agent's directive. */
1145
+ condition: string;
1146
+ /** Evaluations that found the condition unmet. */
1147
+ iterations: number;
1148
+ /** Epoch milliseconds when the goal was set. */
1149
+ setAt: number;
1150
+ /** `restored` when a resume brought back a goal set in an earlier process. */
1151
+ origin: 'user' | 'restored';
1152
+ /** Output tokens the root agent and the goal checks have spent while the goal was set. */
1153
+ outputTokens: number;
1154
+ /** The evaluator's reason from the most recent unmet check. */
1155
+ lastReason?: string;
1156
+ }
1157
+ /** One change in a goal's state, reported to the host as it happens. */
1158
+ type GoalStatusEvent = {
1159
+ state: 'set';
1160
+ condition: string;
1161
+ } | {
1162
+ state: 'cleared';
1163
+ condition: string;
1164
+ } | {
1165
+ state: 'unmet';
1166
+ condition: string;
1167
+ reason: string;
1168
+ iterations: number;
1169
+ } | {
1170
+ state: 'met' | 'failed';
1171
+ condition: string;
1172
+ reason: string;
1173
+ iterations: number;
1174
+ durationMs: number;
1175
+ tokens: number;
1176
+ } | {
1177
+ state: 'paused';
1178
+ condition: string;
1179
+ cause: 'block_cap' | 'check_failed' | 'max_turns';
1180
+ detail?: string;
1181
+ };
1182
+
896
1183
  /** What triggered a compaction attempt. */
897
1184
  type CompactTrigger = 'manual' | 'auto';
898
1185
  /**
@@ -1007,6 +1294,13 @@ interface CompactBoundary {
1007
1294
  timestamp: number;
1008
1295
  /** Earlier user turns kept verbatim ahead of the checkpoint; absent on records written before Carried Turns. */
1009
1296
  carriedCount?: number;
1297
+ /**
1298
+ * The compaction used reduced-fidelity coverage. Live only: the host shows it on the boundary's
1299
+ * row, it is not written to the session record, and a resumed session's boundaries lack it.
1300
+ */
1301
+ degraded?: boolean;
1302
+ /** The compaction's warning, for the same live row as `degraded`; not persisted either. */
1303
+ warning?: string;
1010
1304
  }
1011
1305
  interface RewindCheckpointMetadata {
1012
1306
  snapshotId?: string;
@@ -1019,12 +1313,18 @@ interface RewindTarget extends RewindCheckpointMetadata {
1019
1313
  id: string;
1020
1314
  userEventId: string;
1021
1315
  prompt: string;
1316
+ /**
1317
+ * `prompt` is what the user typed: their text, or the command line that ran a command. Only such
1318
+ * a prompt goes back in the composer on a rewind. Absent for any other turn (a command's body, a
1319
+ * notification) and for one recorded before this was, whose prompt may be a command's body.
1320
+ */
1321
+ promptTyped?: boolean;
1022
1322
  attachments?: ImageAttachment[];
1023
1323
  timestamp: number;
1024
1324
  codeAvailable: boolean;
1025
1325
  }
1026
1326
  interface SessionRecord {
1027
- type: 'user' | 'assistant' | 'local' | 'tool_call' | 'tool_result' | 'usage' | 'session_meta' | 'compact' | 'turn_checkpoint' | 'rewind' | 'plan';
1327
+ type: 'user' | 'assistant' | 'local' | 'tool_call' | 'tool_result' | 'usage' | 'session_meta' | 'compact' | 'turn_checkpoint' | 'rewind' | 'plan' | 'goal';
1028
1328
  eventId?: string;
1029
1329
  timestamp: number;
1030
1330
  data: unknown;
@@ -1044,6 +1344,17 @@ interface PlanRecordData {
1044
1344
  todos?: PersistedTodo[];
1045
1345
  tasks?: AgentTask[];
1046
1346
  }
1347
+ /**
1348
+ * Payload stored in a SessionRecord of type `goal` — last record wins.
1349
+ *
1350
+ * Written whenever the session's `/goal` changes: set, checked unmet, met, failed
1351
+ * or cleared. `goal: null` is a cleared goal, so a resume after a goal was met
1352
+ * does not bring it back. Readers must tolerate absence, as for `plan`.
1353
+ */
1354
+ interface GoalRecordData {
1355
+ version: 1;
1356
+ goal: ActiveGoal | null;
1357
+ }
1047
1358
  interface PersistedTodo {
1048
1359
  content: string;
1049
1360
  status: string;
@@ -1293,6 +1604,8 @@ interface LoadedSession {
1293
1604
  * not survive the restart".
1294
1605
  */
1295
1606
  plan?: PlanRecordData;
1607
+ /** The newest `goal` record; `goal: null` inside it is a goal that was cleared or reached. */
1608
+ goal?: GoalRecordData;
1296
1609
  /**
1297
1610
  * Token totals recorded by earlier processes of this session, summed from the
1298
1611
  * `usage` records. USD is deliberately not stored: pricing changes between
@@ -1342,13 +1655,22 @@ interface SessionStoreInterface {
1342
1655
  list(): SessionMeta[];
1343
1656
  findByName(name: string): SessionMeta | undefined;
1344
1657
  findById(id: string): SessionMeta | undefined;
1345
- mostRecentInCwd(cwd: string): SessionMeta | undefined;
1658
+ /**
1659
+ * The newest session in `cwd` with at least one message, or the newest one `accept` approves
1660
+ * when given; empty sessions are skipped by default.
1661
+ */
1662
+ mostRecentInCwd(cwd: string, accept?: (meta: SessionMeta) => boolean): SessionMeta | undefined;
1346
1663
  saveImageAttachment?(sessionId: string, image: {
1347
1664
  bytes: Uint8Array;
1348
1665
  mediaType: ImageAttachment['mediaType'];
1349
1666
  displayName?: string;
1350
1667
  }): ImageAttachment;
1351
1668
  readImageAttachment?(sessionId: string, attachment: ImageAttachment): Uint8Array;
1669
+ /**
1670
+ * Remove a session nothing was ever recorded in (a bare header, no name), file and index
1671
+ * entry together. Returns whether it was removed. A store that omits it keeps every session.
1672
+ */
1673
+ removeUntouched?(id: string): boolean;
1352
1674
  cleanup(days: number, preserveIds?: ReadonlySet<string>): number;
1353
1675
  }
1354
1676
 
@@ -1393,7 +1715,7 @@ interface ConfigCommandDisplay {
1393
1715
  workspace: string;
1394
1716
  };
1395
1717
  }
1396
- type ContextWindowSource = 'declared' | 'learned' | 'family' | 'default';
1718
+ type ContextWindowSource = 'declared' | 'learned' | 'reported' | 'family' | 'default';
1397
1719
  interface ContextCommandDisplay {
1398
1720
  kind: 'context';
1399
1721
  model: string;
@@ -1432,6 +1754,11 @@ interface UsageCommandDisplay {
1432
1754
  rate?: {
1433
1755
  inputPerMillion: number;
1434
1756
  outputPerMillion: number;
1757
+ longPrompt?: {
1758
+ overPromptTokens: number;
1759
+ inputPerMillion: number;
1760
+ outputPerMillion: number;
1761
+ };
1435
1762
  };
1436
1763
  estimatedCostUsd?: number;
1437
1764
  /** Delegated agents inside `usage`, so the sheet can say so (#370). */
@@ -1660,6 +1987,9 @@ interface AgentRunResult {
1660
1987
  readonly ambient?: AgentRunAmbientSnapshot;
1661
1988
  }
1662
1989
 
1990
+ /** {@link reachesUncPath} for one top-level call, answering from what it already probed. */
1991
+ type UncGuard = (path: string) => boolean;
1992
+
1663
1993
  type SkillSource = 'user' | 'project';
1664
1994
  type SkillRootKind = 'book' | 'agents' | 'claude' | 'opencode';
1665
1995
  type SkillIssueSeverity = 'error' | 'warning';
@@ -1730,8 +2060,16 @@ interface DiscoverSkillsOptions {
1730
2060
  enabled?: boolean;
1731
2061
  executionOverrides?: Readonly<Record<string, SkillExecution>>;
1732
2062
  }
1733
- /** Roots are ordered from lowest to highest precedence. */
1734
- declare function skillRoots(workspace: string, options?: DiscoverSkillsOptions): SkillRoot[];
2063
+ /**
2064
+ * Roots are ordered from lowest to highest precedence.
2065
+ *
2066
+ * A project root is a path a repository can commit as a link, and on Windows resolving a link to
2067
+ * a network share opens the share, which can send the user's credentials to the host it names
2068
+ * (#567, #579). A project root that reaches one is therefore not a root at all, so nothing that
2069
+ * takes its roots from here resolves it. The user's own roots are theirs to keep on a share.
2070
+ * `reachesUnc` is the UNC guard of the discovery this is part of, so the two share what it probed.
2071
+ */
2072
+ declare function skillRoots(workspace: string, options?: DiscoverSkillsOptions, reachesUnc?: UncGuard): SkillRoot[];
1735
2073
  declare function discoverSkills(workspace: string, overrides?: Readonly<Record<string, SkillActivation>>, options?: DiscoverSkillsOptions): Skill[];
1736
2074
  declare function applySkillOverrides(skills: readonly Skill[], overrides: Readonly<Record<string, SkillActivation>>, executionOverrides?: Readonly<Record<string, SkillExecution>>, enabled?: boolean): Skill[];
1737
2075
  declare function loadSkillBody(skill: Skill): LoadedSkillBody;
@@ -2135,11 +2473,12 @@ type AgentRuntimeEvent = {
2135
2473
  /**
2136
2474
  * A line the child's own run raised for the operator, such as a read it was refused (#305
2137
2475
  * item 4). A child's transcript is summarised into a handoff, so a refusal inside it is
2138
- * otherwise invisible to whoever delegated it.
2476
+ * otherwise invisible to whoever delegated it. Without an `agentId` it is the manager's own
2477
+ * one-time line (#579).
2139
2478
  */
2140
2479
  | {
2141
2480
  type: 'agent_notice';
2142
- agentId: string;
2481
+ agentId?: string;
2143
2482
  message: string;
2144
2483
  } | {
2145
2484
  type: 'agent_persistence';
@@ -2216,6 +2555,19 @@ interface AgentApplyResult {
2216
2555
  commit?: string;
2217
2556
  error?: string;
2218
2557
  }
2558
+ /**
2559
+ * Who started an apply: the user (`/agent apply`, `/review --fix`) or the model (`AgentApply`).
2560
+ * The model's `AgentStop` stops only the applies the model started (#527).
2561
+ */
2562
+ type AgentApplyStarter = 'user' | 'model';
2563
+ /** What a stop that kept the user's applies running did with the agent's applies. */
2564
+ interface AgentStopOutcome {
2565
+ agent: AgentRecord;
2566
+ /** The evidence id of each model-started apply it stopped, which has ended. */
2567
+ stoppedApplies: string[];
2568
+ /** The evidence id of each user-started apply it left running. */
2569
+ keptUserApplies: string[];
2570
+ }
2219
2571
 
2220
2572
  /**
2221
2573
  * A small, always-current file saying what a run is doing right now.
@@ -2549,6 +2901,14 @@ interface AgentLoopCallbacks {
2549
2901
  onAgentEvent?: (event: AgentRuntimeEvent) => void;
2550
2902
  /** Called when a host or system notice should be surfaced (e.g. memory candidate saved). */
2551
2903
  onNotice?: (notice: string) => void;
2904
+ /** Called when the session's `/goal` changes state at a stop (checked, met, failed, paused). */
2905
+ onGoalStatus?: (event: GoalStatusEvent) => void;
2906
+ /**
2907
+ * Spend on a side request the loop or a tool made on another model -- a goal check, an
2908
+ * `Advisor` consultation. Not the turn's own usage, so a host must not feed it to the
2909
+ * context meter; it belongs on the session's bill, priced at `metadata`'s model.
2910
+ */
2911
+ onAuxiliaryUsage?: (usage: Usage, metadata: ProviderResponseMetadata, source: string) => void;
2552
2912
  }
2553
2913
 
2554
2914
  interface ProviderStreamOptions {
@@ -2850,10 +3210,80 @@ interface Sandbox {
2850
3210
  *
2851
3211
  * Returns null if the sandbox is unavailable.
2852
3212
  */
2853
- wrap(command: string, workspaceRoot: string): CommandExecution | null;
3213
+ wrap(command: string, workspaceRoot: string, protectedPaths?: ProtectedWorkspacePaths): CommandExecution | null;
2854
3214
  /** One-line description of the policy actually enforced, for diagnostics. */
2855
3215
  describe(): string;
2856
3216
  }
3217
+ /**
3218
+ * One workspace path the namespace must shield, and how.
3219
+ *
3220
+ * - `directory` — a directory the host reads after the command exits. Bound
3221
+ * read-only when it exists, masked with an empty read-only tmpfs when it does
3222
+ * not, which both hides any contents the host grows there later and makes
3223
+ * creating it fail.
3224
+ * - `file` — a file the host reads after the command exits. Bound read-only
3225
+ * when it exists: bubblewrap aborts the whole invocation on a tmpfs mounted
3226
+ * over a file path ("Not a directory"), so an absent file cannot be masked at
3227
+ * all.
3228
+ * - `hidden-file` — a file whose *contents* must not reach the command, bound
3229
+ * over with `/dev/null`. A read-only bind of `.book/` still serves what is in
3230
+ * it, and the namespace shares the host network, so a key the user stored
3231
+ * there would be one `cat` away from a sandboxed command.
3232
+ */
3233
+ type ProtectedMountKind = 'directory' | 'file' | 'hidden-file';
3234
+ interface ProtectedMount {
3235
+ /**
3236
+ * The path the namespace mounts at, in the workspace's own spelling;
3237
+ * containment is decided on canonical paths.
3238
+ */
3239
+ path: string;
3240
+ kind: ProtectedMountKind;
3241
+ /** False when nothing exists at it, which for a directory becomes a mask. */
3242
+ present: boolean;
3243
+ }
3244
+ interface ProtectedWorkspacePaths {
3245
+ /**
3246
+ * Directories the namespace must pin as mount points in their own right.
3247
+ *
3248
+ * A read-only child mount is not enough: the directory that holds it can still
3249
+ * be renamed away and replaced, and rename(2) on a path that is not a mount
3250
+ * point succeeds. `--bind <dir> <dir>` pins it, which is what makes
3251
+ * `mv .git .git.old` fail with EBUSY instead of redirecting the host's git.
3252
+ *
3253
+ * Only the workspace's own top-level `.git` is pinned, and only in the opt-out
3254
+ * mode (`allowGitWrites: true`) where git directories stay writable. Read-only
3255
+ * mode has no pin because the git directory is already a mount point through
3256
+ * its own `--ro-bind`, and the half-done `git worktree remove` it describes
3257
+ * (deleting the work tree and failing on the read-only admin directory) now
3258
+ * happens by design in read-only mode.
3259
+ */
3260
+ pinnedDirectories: string[];
3261
+ mounts: ProtectedMount[];
3262
+ /**
3263
+ * Control paths this namespace cannot protect at all. A refusal rather than a
3264
+ * mount: bwrap aborts the whole invocation on the shapes that could be
3265
+ * mistaken for one, and guessing would leave a control file writable while
3266
+ * reporting a sandbox.
3267
+ */
3268
+ refusals: ControlPathRefusal[];
3269
+ /**
3270
+ * Control mounts inside read-only git dirs (hooks, config, etc.), preserved in
3271
+ * read-only mode so an allowWrite opt-in on a git dir can re-apply them.
3272
+ */
3273
+ gitDirControlMounts: ProtectedMount[];
3274
+ }
3275
+ /**
3276
+ * Why a workspace cannot be sandboxed, and which path says so.
3277
+ *
3278
+ * Both are shapes a repository controls and no mount can cover. The run is
3279
+ * refused with the path named, because the fix — replace the link, trim the
3280
+ * config — is a change to the workspace, not to a Book setting.
3281
+ */
3282
+ type ControlPathRefusalReason = 'symlinked-control-path' | 'oversized-git-config' | 'workspace-root-is-git-dir';
3283
+ interface ControlPathRefusal {
3284
+ path: string;
3285
+ reason: ControlPathRefusalReason;
3286
+ }
2857
3287
 
2858
3288
  interface SessionRuntimeOptions {
2859
3289
  tasks?: AgentTask[];
@@ -2869,18 +3299,17 @@ interface SessionRuntimeOptions {
2869
3299
  skillRegistry?: SkillRegistry;
2870
3300
  /** Skill root discovery options, shared by the registry and the watcher so they agree. */
2871
3301
  skillDiscoveryOptions?: DiscoverSkillsOptions;
2872
- /**
2873
- * The conversation this runtime is being built for. Its tool calls seed
2874
- * `usedToolNames`, so every runtime built for a loaded, resumed, or rewound
2875
- * conversation knows what that conversation already read.
2876
- */
2877
- history?: Message[];
2878
3302
  /**
2879
3303
  * Whether this runtime is the session's long-lived one. `false` marks a runtime that is disposed
2880
3304
  * when one run ends — a Task subagent's, a managed agent's — and so may not adopt a timed-out
2881
3305
  * command as a background shell it would then destroy. Defaults to `true`.
2882
3306
  */
2883
3307
  ownsSessionShells?: boolean;
3308
+ /**
3309
+ * The store instances of every agent manager this session has disposed. Hand on the outgoing
3310
+ * runtime's set to its replacement; see {@link SessionRuntime.disposedAgentInstanceIds}.
3311
+ */
3312
+ disposedAgentInstanceIds?: Set<string>;
2884
3313
  }
2885
3314
  /** Mutable resources owned by one logical agent session. */
2886
3315
  declare class SessionRuntime {
@@ -2897,6 +3326,18 @@ declare class SessionRuntime {
2897
3326
  * Surfaced once in `<session-state>` so a dropped plan is a visible event.
2898
3327
  */
2899
3328
  planUnrestored: boolean;
3329
+ /**
3330
+ * The session's `/goal`, if one is set. Read by the root loop each time the agent
3331
+ * stops without calling a tool; mutated there and by the host's `/goal` command.
3332
+ * A runtime is replaced on `/clear`, which is what clears the goal with it.
3333
+ */
3334
+ activeGoal?: ActiveGoal;
3335
+ /**
3336
+ * The loaded memory snapshot whose `MEMORY.md` was last seeded into the observation ledger, so
3337
+ * the seed happens once per snapshot: a later turn must not claim the model has seen a file that
3338
+ * changed since. A reloaded snapshot (a new object) is seeded again, as a resumed session is.
3339
+ */
3340
+ memoryIndexSeededFor?: object;
2900
3341
  readonly backgroundShells: BackgroundShellStore;
2901
3342
  readonly fileObservationLedger: Map<string, FileObservation>;
2902
3343
  readonly toolDiscoveryState: ToolDiscoveryState;
@@ -2937,13 +3378,6 @@ declare class SessionRuntime {
2937
3378
  * there, as it was before #302.
2938
3379
  */
2939
3380
  readonly ownsSessionShells: boolean;
2940
- /**
2941
- * Canonical names of tools executed in this conversation, seeded from the
2942
- * conversation it was built for. A runtime is replaced rather than cleared
2943
- * when the host projects a different conversation, so this only ever grows
2944
- * within one conversation.
2945
- */
2946
- readonly usedToolNames: Set<string>;
2947
3381
  /**
2948
3382
  * Files this session's tool output was clipped into, as absolute paths.
2949
3383
  *
@@ -2954,6 +3388,15 @@ declare class SessionRuntime {
2954
3388
  */
2955
3389
  readonly clippedOutputPaths: Set<string>;
2956
3390
  agentManager?: AgentManager;
3391
+ /**
3392
+ * The store instances of the agent managers this session has disposed, its earlier runtimes'
3393
+ * included: one set, handed from each runtime to the next by `AgentSession.replaceRuntime`.
3394
+ * Every manager built on this runtime gives it to its store, which adds its own instance when it
3395
+ * is disposed and reads each instance in it as gone, whatever its lease file says (#568). A lease
3396
+ * outlives its dispose when the unlink fails (a Windows scanner holding the file), and would read
3397
+ * as a live owner of the records that manager wrote for up to 15 s.
3398
+ */
3399
+ readonly disposedAgentInstanceIds: Set<string>;
2957
3400
  private readonly abortControllers;
2958
3401
  private readonly timers;
2959
3402
  private readonly childProcesses;
@@ -3025,6 +3468,13 @@ declare function runAgentLoop(config: AgentConfig, registry: ToolRegistry, userM
3025
3468
  userMessageKind?: Message['kind'];
3026
3469
  /** The prompt was resolved from a command or delegated, not typed by the user. */
3027
3470
  userMessageDerived?: boolean;
3471
+ /**
3472
+ * The text the user typed for this turn, when the turn comes from their typing: what they sent,
3473
+ * or the command line that ran a command. Only its `$name` tokens invoke a skill by name; a
3474
+ * token anywhere else in the prompt (a command's body, an inlined file, a notification, a
3475
+ * delegated task) is plain text. Absent, no skill is invoked by name.
3476
+ */
3477
+ typedText?: string;
3028
3478
  /** Synthetic host notifications bypass user-authored prompt hooks and memory capture. */
3029
3479
  skipUserPromptHooks?: boolean;
3030
3480
  /** Host identity for each streamed assistant turn. */
@@ -3118,9 +3568,34 @@ declare function createAgentWorktree(snapshot: AgentSnapshot, agentId: string, w
3118
3568
  declare function removeSnapshotRef(snapshot: AgentSnapshot): Promise<void>;
3119
3569
  declare function checkoutAgentCommit(worktree: string, commit: string): Promise<void>;
3120
3570
  declare function commitAgentWork(record: AgentRecord, snapshot: AgentSnapshot): Promise<PatchCandidate | undefined>;
3121
- declare function applyVerifiedCandidate(snapshot: AgentSnapshot, candidate: PatchCandidate, signal?: AbortSignal): Promise<AgentApplyResult>;
3571
+ /**
3572
+ * What a caller can say about how an apply may be stopped, beyond the `signal` it is given.
3573
+ *
3574
+ * `signal` stops the apply's **reads**: the candidate resolution, the pending-pick check, the
3575
+ * fingerprint, the delta, the pre-check and the signing configuration read. Those are where an
3576
+ * operator's slow `gpg.ssh.defaultKeyCommand` can hold an apply for up to 30 s per call (#491), and
3577
+ * none of them has written anything, so a stop there leaves nothing behind.
3578
+ *
3579
+ * `mutationSignal` stops the **mutation step**: the `cherry-pick`, or the `git apply -` of a dirty
3580
+ * workspace. It is separate because stopping that step is not free: a stop SIGTERMs the pick and
3581
+ * relies on the unsignalled `cherry-pick --abort` that follows to put the operator's repository
3582
+ * back, and a caller whose process exits right after it aborts (`dispose()`) never lets that
3583
+ * cleanup run. A caller that wants the reads stopped and nothing after them passes a signal here
3584
+ * that it never aborts. Absent, the mutation step uses `signal`, so a caller that passes one signal
3585
+ * gets one behaviour for all of it.
3586
+ */
3587
+ interface ApplyCandidateOptions {
3588
+ mutationSignal?: AbortSignal;
3589
+ }
3590
+ declare function applyVerifiedCandidate(snapshot: AgentSnapshot, candidate: PatchCandidate, signal?: AbortSignal, options?: ApplyCandidateOptions): Promise<AgentApplyResult>;
3122
3591
 
3123
3592
  type AtomicWriteOperation = 'lock' | 'serialize' | 'write' | 'fsync' | 'rename';
3593
+ /**
3594
+ * Outcome of an atomic JSON write.
3595
+ *
3596
+ * `attempts` counts how many times this write tried: 1 for a clean write, plus 1 for each contended
3597
+ * retry of either the lock step or the rename step. A stale-lock reclaim is not a retry.
3598
+ */
3124
3599
  type AtomicWriteResult = {
3125
3600
  status: 'ok';
3126
3601
  target: string;
@@ -3155,6 +3630,11 @@ type AtomicWriteResult = {
3155
3630
  };
3156
3631
  /** What one {@link AtomicJsonWriter.writeAsync} call is allowed to decide for itself. */
3157
3632
  interface AtomicAsyncWriteOptions {
3633
+ /**
3634
+ * A complete temp file an earlier write left behind, renamed over the target instead of writing a
3635
+ * new one: the `preparedTemp` of {@link AtomicJsonWriter.write}.
3636
+ */
3637
+ preparedTemp?: string;
3158
3638
  /**
3159
3639
  * Asked immediately before the rename, which is the only step of the write that changes the
3160
3640
  * target.
@@ -3178,6 +3658,17 @@ interface AtomicJsonWriterOptions {
3178
3658
  instanceId: string;
3179
3659
  pid?: number;
3180
3660
  hostname: string;
3661
+ /**
3662
+ * The budget of one write, counted on `now` from before the lock is taken: the lock, the
3663
+ * fsyncs of the lock and temp files, and the rename retries all spend it, so on a slow disk the
3664
+ * fsyncs alone can use it up. The budget bounds the back-off ladder, not the work already done,
3665
+ * so a rename that hits contention is always retried once after its back-off, even when the
3666
+ * budget is gone by then; after that retry the deadline applies and the write returns `busy`.
3667
+ * That back-off is the clamped ladder step while the budget lasts, and with little or none left
3668
+ * it is the first rename step at least and the current step at most. A write whose rename
3669
+ * fails every time therefore ends within the budget plus one such back-off. The lock phase has no such retry: a lock still contended at the
3670
+ * deadline is `busy`.
3671
+ */
3181
3672
  deadlineMs?: number;
3182
3673
  now?: () => number;
3183
3674
  randomId?: () => string;
@@ -3261,8 +3752,30 @@ declare class AtomicJsonWriter {
3261
3752
  *
3262
3753
  * {@link AtomicAsyncWriteOptions.canCommit} is the one thing this path has that the synchronous
3263
3754
  * one does not, and it exists because this path is the one a caller can no longer wait for.
3755
+ * Without it the write cannot be withdrawn, so the result is never `cancelled`.
3264
3756
  */
3265
- writeAsync(target: string, value: unknown, options?: AtomicAsyncWriteOptions): Promise<AtomicWriteResult>;
3757
+ writeAsync(target: string, value: unknown, options?: Omit<AtomicAsyncWriteOptions, 'canCommit'>): Promise<Exclude<AtomicWriteResult, {
3758
+ status: 'cancelled';
3759
+ }>>;
3760
+ writeAsync(target: string, value: unknown, options: AtomicAsyncWriteOptions): Promise<AtomicWriteResult>;
3761
+ /** What both write paths record in a lock file they take: who holds it, and since when. */
3762
+ private lockOwnerDocument;
3763
+ /** A temp file name unique to this writer, which recovery parses back to its target and owner. */
3764
+ private tempPathFor;
3765
+ /**
3766
+ * Whether a contended lock may be taken from its holder, given what its file said: a lock with a
3767
+ * valid owner only when that owner is gone, one without once it is older than the stale window.
3768
+ */
3769
+ private mayReclaim;
3770
+ /**
3771
+ * How long to back off before `attempt`: the ladder step, cut short by the time left to the
3772
+ * deadline. `guaranteed` is the one retry past the deadline, after the first contended rename.
3773
+ * Cut to nothing, that retry would hit the same contention again; so it waits at least the first
3774
+ * rename step, whatever is left, and never more than the ladder step: the ladder is shared with
3775
+ * the lock phase, and a step it has climbed to could block the main thread for a long time past
3776
+ * the deadline.
3777
+ */
3778
+ private backoffMs;
3266
3779
  private pause;
3267
3780
  private pauseAsync;
3268
3781
  private reclaimStaleLockAsync;
@@ -3297,6 +3810,12 @@ interface AgentStoreOptions {
3297
3810
  eventSink?: (event: AgentStorePersistenceEvent) => void;
3298
3811
  writer?: AtomicJsonWriter;
3299
3812
  writerOptions?: Omit<AtomicJsonWriterOptions, 'instanceId' | 'hostname'>;
3813
+ /**
3814
+ * The instances of the stores the session this one belongs to has disposed, shared rather than
3815
+ * copied: this store adds its own when it is disposed, and reads each one in it as gone, whatever
3816
+ * its lease says (#568). See {@link AgentStore.aliveByLease}.
3817
+ */
3818
+ disposedInstanceIds?: Set<string>;
3300
3819
  }
3301
3820
  declare class AgentStore {
3302
3821
  readonly directory: string;
@@ -3320,8 +3839,14 @@ declare class AgentStore {
3320
3839
  private readonly processAlive;
3321
3840
  private readonly eventSink?;
3322
3841
  private readonly writer;
3842
+ private readonly disposedInstanceIds?;
3323
3843
  private enabled;
3324
3844
  private disposed;
3845
+ /**
3846
+ * Set while {@link open} loads the store. The writes `load` finds it needs are queued rather than
3847
+ * made, and `open` flushes them on the thread pool once loading is done.
3848
+ */
3849
+ private opening;
3325
3850
  private heartbeatTimer?;
3326
3851
  /**
3327
3852
  * The heartbeat write in flight, if any. See {@link refreshLease}: a tick that arrives while one
@@ -3337,13 +3862,53 @@ declare class AgentStore {
3337
3862
  private readonly evidence;
3338
3863
  private readonly snapshots;
3339
3864
  private readonly pendingWrites;
3340
- constructor(repoHash: string, root?: string, enabled?: boolean, options?: AgentStoreOptions);
3865
+ /**
3866
+ * A store that has not touched the disk yet, and is not usable until it has: {@link open} is the
3867
+ * way to get one.
3868
+ */
3869
+ private constructor();
3870
+ /**
3871
+ * Open the store for `repoHash` under `root`: create its directories, take this instance's lease
3872
+ * and start the heartbeat, then recover the temp files a crashed write left behind, load what is
3873
+ * persisted, and make the writes loading found it needed.
3874
+ *
3875
+ * All of that is on the thread pool. It is the first thing the TUI does after its first frame, and
3876
+ * done synchronously on a slow disk its `open`s and `fsync`s held the event loop for hundreds of
3877
+ * milliseconds: keys typed in that window arrived in one read and were taken for a paste (#516).
3878
+ *
3879
+ * The lease comes before the first read, because on the thread pool the reads no longer run in
3880
+ * one piece: a disposed manager's {@link recordLateApplication} can land between them. It
3881
+ * writes only when no other instance holds a live lease, so with the lease already on disk it
3882
+ * stands down; taken after loading, it found none, wrote a record this store had already read,
3883
+ * and this store's next save of that stale copy undid it.
3884
+ *
3885
+ * The promise resolves only once the lease is on disk, so no caller ever holds a store without one
3886
+ * (#357). A store whose directories cannot be created, or whose contents cannot be read, is not an
3887
+ * error: it resolves disabled, and runs in memory only.
3888
+ */
3889
+ static open(repoHash: string, root?: string, enabled?: boolean, options?: AgentStoreOptions): Promise<AgentStore>;
3890
+ private initialize;
3891
+ /**
3892
+ * Turn an opening store whose contents could not be read into a disabled one: nothing of what it
3893
+ * read is kept, nothing it queued is written, and the lease it took is removed with its heartbeat,
3894
+ * since a disabled store writes nothing that a lease would speak for.
3895
+ */
3896
+ private disableAfterFailedLoad;
3341
3897
  private ensureDirectories;
3342
3898
  private safeLog;
3899
+ private quarantinePath;
3343
3900
  private quarantine;
3901
+ /** {@link quarantine}, on the thread pool, for what {@link open} reads. */
3902
+ private quarantineAsync;
3344
3903
  private readJson;
3904
+ /** {@link readJson}, on the thread pool, for what {@link open} reads. */
3905
+ private readJsonAsync;
3345
3906
  private targetMatchesId;
3346
3907
  private loadDirectory;
3908
+ /**
3909
+ * Read what is persisted into memory. Runs only inside {@link open}, so the writes it finds it
3910
+ * needs are queued, and `open` makes them once it is done.
3911
+ */
3347
3912
  private load;
3348
3913
  private migrateAgent;
3349
3914
  private agentPath;
@@ -3358,6 +3923,11 @@ declare class AgentStore {
3358
3923
  private cancelPendingWrite;
3359
3924
  private queueWrite;
3360
3925
  private flushPending;
3926
+ /** {@link flushPending} on the thread pool: the writes {@link open} makes. */
3927
+ private flushPendingAsync;
3928
+ private beginFlush;
3929
+ /** What both flushes do with the result: settle the pending write, or schedule its retry. */
3930
+ private finishFlush;
3361
3931
  private schedulePending;
3362
3932
  private enterDegraded;
3363
3933
  private maybeRecover;
@@ -3371,6 +3941,45 @@ declare class AgentStore {
3371
3941
  defer?: boolean;
3372
3942
  required?: boolean;
3373
3943
  }): AtomicWriteResult;
3944
+ /**
3945
+ * Record on disk how an apply that outlived its manager ended: the one fact that can only be
3946
+ * known after this store was disposed. A pick that finished after `dispose()` is on the
3947
+ * operator's branch, and nothing else records that.
3948
+ *
3949
+ * The session's next manager may have opened this store's directory by then and written the
3950
+ * same record (#518), so this is a field-level update rather than a save of the disposed
3951
+ * manager's copy: the record is re-read from disk and only its `applicationStatus` changes, with
3952
+ * the owner and `updatedAt` stamped as every write stamps them. Written to disk only, since the
3953
+ * disposed store's cache has no reader left.
3954
+ *
3955
+ * Nothing is written, and the record is left as it is, when another instance holds a live lease
3956
+ * here (the record is its to write now), when the record on disk is newer than `seenUpdatedAt`,
3957
+ * the `updatedAt` of the copy the disposed manager last had (someone wrote it since), or when
3958
+ * the record is not on disk.
3959
+ *
3960
+ * That is decided twice: once before the write, and again immediately before each rename, which
3961
+ * is the step that changes the file. Between the two are a lock, a temp file and its fsync, and a
3962
+ * save by the next manager or another process can land in that window (its writer reclaims the
3963
+ * lock of a store that is gone); the copy read before it would otherwise be renamed over it.
3964
+ */
3965
+ recordLateApplication(agentId: string, status: AgentRecord['applicationStatus'], seenUpdatedAt: number): Promise<'recorded' | 'skipped' | 'failed'>;
3966
+ /**
3967
+ * What one write of {@link recordLateApplication} not landing makes of it, logged without any of
3968
+ * the record's content; `undefined` when the write landed.
3969
+ */
3970
+ private lateWriteSetback;
3971
+ /**
3972
+ * Why a late write to the record at `path` must not land now, or `undefined` if it may: the skip
3973
+ * rule of {@link recordLateApplication}, with `newestAllowed` the `updatedAt` the record on disk
3974
+ * may have. Synchronous, because it is asked as the `canCommit` of a write, and an answer that
3975
+ * waited for the thread pool would be one the rename no longer immediately follows. It is a
3976
+ * directory listing and a few small reads, once per late apply.
3977
+ */
3978
+ private lateWriteConflict;
3979
+ /** Whether an instance other than this one holds a live lease in `instances/`. */
3980
+ private anotherInstanceIsLive;
3981
+ /** Whether `lease`, as read from `instances/`, is another instance's, and live. */
3982
+ private isLiveForeignLease;
3374
3983
  removeAgent(agentId: string): void;
3375
3984
  removePlan(planId: string): void;
3376
3985
  removeEvidence(evidenceId: string): void;
@@ -3383,6 +3992,11 @@ declare class AgentStore {
3383
3992
  recoverAbandonedAgents(): AgentRecord[];
3384
3993
  markActiveInterrupted(): AgentRecord[];
3385
3994
  isOwnedByCurrent(agentId: string): boolean;
3995
+ /**
3996
+ * The instance that wrote the newest copy of the record this store knows, as
3997
+ * {@link isOwnedByLiveForeign} reads it.
3998
+ */
3999
+ ownerInstanceId(agentId: string): string | undefined;
3386
4000
  isOwnedByLiveForeign(agentId: string): boolean;
3387
4001
  hasPendingAgent(agentId: string): boolean;
3388
4002
  getPersistenceState(): 'healthy' | 'degraded_busy' | 'degraded_unavailable';
@@ -3392,13 +4006,16 @@ declare class AgentStore {
3392
4006
  dispose(): void;
3393
4007
  private refreshForeignSummaries;
3394
4008
  /**
3395
- * Take the lease for this instance, before anything else reads it.
4009
+ * Take the lease for this instance, before this store reads anything and before anything else
4010
+ * reads it.
3396
4011
  *
3397
- * This first lease is written synchronously where the heartbeats below are not, because it is the
3398
- * one a caller depends on the moment the constructor returns: a store that has just been built is
3399
- * asked whether its own instance is alive, and answered from this file. Deferring it to the thread
3400
- * pool leaves a window in which a brand new store has no lease at all, which is what a manager
3401
- * reading a just-created store sees when its first agent looks abandoned (#357).
4012
+ * {@link open} waits for this write before it resolves, and that order is the guarantee: a store
4013
+ * that has just been opened is asked whether its own instance is alive, and answered from this
4014
+ * file. A store handed out before its lease landed would have a window with no lease at all, which
4015
+ * is what a manager reading a just-created store saw when its first agent looked abandoned (#357).
4016
+ * The write itself is on the thread pool like the heartbeat's, because on a slow disk its lock,
4017
+ * fsyncs and rename are what held the TUI's event loop after its first frame (#516): the caller
4018
+ * waits for it, the event loop does not.
3402
4019
  *
3403
4020
  * A failure is not fatal here either, for the reason it is not fatal on the heartbeat: what is
3404
4021
  * written is this instance's own ownership record, and every other write carries the same metadata.
@@ -3412,7 +4029,7 @@ declare class AgentStore {
3412
4029
  * every one of them a disk round trip, all of it on whichever thread scheduled the timer — for the
3413
4030
  * TUI, the event loop the keystrokes and the model stream are on. On a busy disk that is hundreds
3414
4031
  * of milliseconds every five seconds, and it lands on the user rather than on the heartbeat
3415
- * (#357). {@link takeLease} is the exception, and says why.
4032
+ * (#357). {@link takeLease} is on the thread pool too, but awaited: see there.
3416
4033
  *
3417
4034
  * Two properties come with moving it off the thread and are the reason this is not simply a
3418
4035
  * fire-and-forget:
@@ -3433,11 +4050,55 @@ declare class AgentStore {
3433
4050
  private refreshLease;
3434
4051
  private leaseDocument;
3435
4052
  private removeLease;
4053
+ /**
4054
+ * {@link removeLease}, for a store that is done with its lease. A lease the unlink leaves behind
4055
+ * (a Windows scanner holding the file, say) reads as fresh for up to {@link LEASE_FRESH_MS}, so
4056
+ * it is overwritten with a dead one, best effort (#568). This session's own later stores read the
4057
+ * instance as gone either way: see {@link aliveByLease}.
4058
+ */
4059
+ private releaseLease;
4060
+ /**
4061
+ * Overwrite this instance's lease with one that reads as dead to every store: a heartbeat stale on
4062
+ * any clock, and no process, so that a store of another process on this host does not fall back
4063
+ * to this one's pid, which is still alive (see {@link isLiveForeignLease}). In place, not by temp
4064
+ * file and rename like every other lease write: a file whose unlink was refused refuses a rename
4065
+ * onto it the same way, but may still take a write. A reader that catches the write half done
4066
+ * reads no lease, which is no fresher than a dead one.
4067
+ */
4068
+ private markLeaseStale;
3436
4069
  private scheduleHeartbeat;
3437
4070
  private defaultProcessAlive;
3438
4071
  private isInstanceAlive;
4072
+ /** {@link isInstanceAlive}, reading the lease on the thread pool, for {@link open}. */
4073
+ private isInstanceAliveAsync;
4074
+ private instanceLeasePath;
4075
+ /**
4076
+ * Whether an instance other than this one is alive, given its lease as read and the owner a
4077
+ * record names. Every caller has already answered for this store's own instance.
4078
+ *
4079
+ * A fresh lease is alive. Without one, an owner on this host is alive while its process is (a
4080
+ * Book from before leases, or one whose heartbeat is late), except when that process is this one
4081
+ * (#544). Every live store takes its lease before anything else and keeps it fresh until
4082
+ * `dispose()` removes it, so an instance of this process without one is taken for a disposed
4083
+ * sibling: the manager a `/clear`, `/resume` or `/rewind` replaced. Its records are the next
4084
+ * manager's to write and recover, and its locks are left over. No registry of disposed instances
4085
+ * is needed for that, and an earlier process whose pid this one now has reads as gone too, which
4086
+ * it is. The cost is a live sibling whose lease is missing or late: its first lease write failed,
4087
+ * or its heartbeat stalled past {@link LEASE_FRESH_MS}. It reads as gone until its next
4088
+ * heartbeat lands. Two live stores on one repository in one process come only from an SDK host
4089
+ * running sessions side by side; the TUI and print mode dispose a runtime before replacing it.
4090
+ *
4091
+ * What that rule misses is a disposed sibling whose lease outlived its dispose: the unlink failed
4092
+ * (a Windows scanner holding the file), and the lease reads as fresh for up to
4093
+ * {@link LEASE_FRESH_MS}. So an instance the session has disposed, one in
4094
+ * {@link AgentStoreOptions.disposedInstanceIds}, is gone whatever its lease says (#568). The set
4095
+ * holds only this session's own instances, so a live instance of another process, or one another
4096
+ * session in this process has not disposed, is judged as before.
4097
+ */
4098
+ private aliveByLease;
3439
4099
  private isLockOwnerAlive;
3440
4100
  private parseTemp;
4101
+ /** Promote or discard the temp files crashed writes left behind. Runs only inside {@link open}. */
3441
4102
  private recoverTemps;
3442
4103
  }
3443
4104
 
@@ -3459,7 +4120,9 @@ interface ManagerOptions {
3459
4120
  runtime?: SessionRuntime;
3460
4121
  permissionMode?: string;
3461
4122
  persistPermissionRule?: (rule: string) => void;
3462
- createStore?: (repoHash: string, root: string | undefined, enabled: boolean) => AgentStore;
4123
+ createStore?: (repoHash: string, root: string | undefined, enabled: boolean,
4124
+ /** What the manager hands every store it opens: its session's disposed instances (#568). */
4125
+ options: Pick<AgentStoreOptions, 'disposedInstanceIds'>) => AgentStore | Promise<AgentStore>;
3463
4126
  }
3464
4127
  interface SubscribeOptions {
3465
4128
  snapshot?: boolean;
@@ -3486,23 +4149,40 @@ declare class AgentManager {
3486
4149
  private readonly snapshotPromises;
3487
4150
  private readonly queue;
3488
4151
  private readonly controllers;
4152
+ /**
4153
+ * Applies in flight, per agent. An apply runs after its agent has finished, so no run controller
4154
+ * covers it; `stop` and `dispose` abort these instead. A set because one agent can be applied
4155
+ * more than once at a time.
4156
+ *
4157
+ * Two controllers because the two ways in are not the same stop. `stop` ends the whole apply,
4158
+ * including a cherry-pick that is running, whose rollback it then waits for (`settled`).
4159
+ * `dispose` ends only the reads: it is synchronous and the process usually exits right after it,
4160
+ * so a pick it killed would never be rolled back and the operator's repository would be left
4161
+ * mid-pick. The pick that is running finishes, bounded by its own timeout.
4162
+ */
4163
+ private readonly applies;
3489
4164
  private readonly questionResolvers;
3490
4165
  private readonly permissionResolvers;
3491
4166
  private readonly permissionRules;
3492
4167
  private readonly activities;
3493
4168
  private readonly waiters;
3494
4169
  private readonly idleWaiters;
4170
+ /** Settles each `wait` still pending, so a disposed manager leaves none of their timers armed. */
4171
+ private readonly pendingWaits;
3495
4172
  private active;
3496
4173
  private interactivePermissions;
3497
4174
  private permissionMode;
3498
4175
  private permissionModeOverride?;
3499
4176
  private readonly subscribers;
4177
+ private readonly heldNotices;
3500
4178
  private legacyEventSink?;
3501
4179
  private readonly textBuffers;
3502
4180
  private readonly textTimers;
3503
4181
  private hookEventSink?;
3504
4182
  private exitHandler?;
3505
4183
  private disposed;
4184
+ /** The session's current manager, asked when an apply lands after dispose (#540). */
4185
+ private lateApplicationReceiver?;
3506
4186
  private persistenceState;
3507
4187
  constructor(config: AgentConfig, parentDefinitions: ToolDefinition[], options?: ManagerOptions);
3508
4188
  setEventSink(sink?: (event: AgentRuntimeEvent) => void, hookSink?: (event: string, payload: Record<string, unknown>) => void): void;
@@ -3535,6 +4215,27 @@ declare class AgentManager {
3535
4215
  private recordForReturn;
3536
4216
  private assertMutable;
3537
4217
  getPersistenceState(): 'healthy' | 'degraded_busy' | 'degraded_unavailable';
4218
+ /**
4219
+ * The user's home directory when this workspace is strictly below it, has no repository of its
4220
+ * own, and home holds a `.git`: the dotfiles repository Book has ignored since #572.
4221
+ */
4222
+ private homeDotfilesAbove;
4223
+ /**
4224
+ * A project below the user's home with no repository of its own used to resolve to the home
4225
+ * directory's, so its records, worktrees and snapshot refs were keyed by it (#579). Since #572
4226
+ * that `.git` is a dotfiles repository Book ignores, and that state is no longer found. Say so
4227
+ * once, with the way to clear it; never read it back, and never move or delete it. Records carry
4228
+ * no workspace, so every record under the home key is counted, the home repository's own
4229
+ * included.
4230
+ */
4231
+ private reportOrphanedHomeState;
4232
+ /**
4233
+ * The manager's own one-time line for the operator. Held until a host is listening, because the
4234
+ * first initialization can run before the TUI subscribes (or between its resubscribes), and a
4235
+ * notice nobody saw was not reported.
4236
+ */
4237
+ private emitManagerNotice;
4238
+ private flushHeldNotices;
3538
4239
  private handlePersistenceEvent;
3539
4240
  /**
3540
4241
  * Turn a write's result into either "it is on disk" or the error an operator sees, and report the
@@ -3593,8 +4294,20 @@ declare class AgentManager {
3593
4294
  get(agentId: string): Promise<AgentRecord | undefined>;
3594
4295
  dismiss(agentId: string): Promise<void>;
3595
4296
  send(agentId: string, message: string, evidenceIds?: string[], runContext?: Pick<AgentRunContext, 'rootRunId' | 'runId'>): Promise<AgentRecord>;
3596
- wait(agentId: string, timeoutMs?: number): Promise<AgentRecord>;
4297
+ wait(agentId: string, timeoutMs?: number, signal?: AbortSignal): Promise<AgentRecord>;
4298
+ /**
4299
+ * Stop an agent's run, and every apply of its result that is running.
4300
+ *
4301
+ * `keepUserApplies` is the model's `AgentStop` (#527): it stops only the applies the model
4302
+ * started with `AgentApply`, leaves running the ones the user started (`/agent apply`,
4303
+ * `/review --fix`), and says which it did with each. Every other caller is the user's own stop,
4304
+ * or a host's, and stops them all.
4305
+ */
3597
4306
  stop(agentId: string, reason?: string): Promise<AgentRecord>;
4307
+ stop(agentId: string, reason: string | undefined, options: {
4308
+ keepUserApplies: true;
4309
+ }): Promise<AgentStopOutcome>;
4310
+ private stopAgent;
3598
4311
  publishEvidence(agentId: string, input: PublishEvidenceInput): Promise<EvidenceItem>;
3599
4312
  listEvidence(options?: {
3600
4313
  requesterAgentId?: string;
@@ -3602,7 +4315,63 @@ declare class AgentManager {
3602
4315
  ids?: string[];
3603
4316
  }): Promise<EvidenceItem[]>;
3604
4317
  reviewEvidence(reviewerAgentId: string, evidenceId: string, verdict: 'pass' | 'fail' | 'inconclusive', notes?: string): Promise<EvidenceItem>;
3605
- apply(agentId: string, evidenceId?: string): Promise<AgentApplyResult>;
4318
+ apply(agentId: string, evidenceId?: string, options?: {
4319
+ signal?: AbortSignal;
4320
+ startedBy?: AgentApplyStarter;
4321
+ }): Promise<AgentApplyResult>;
4322
+ /** Record how an apply of `record`'s candidate ended: on the record, to subscribers, in telemetry. */
4323
+ private recordApplication;
4324
+ /**
4325
+ * Name the manager an apply that outlives this one hands its outcome to: the session's current
4326
+ * manager, asked when the pick lands rather than now, since a session can switch again in the
4327
+ * meantime and creates its manager lazily. Called where a session's runtime is replaced.
4328
+ */
4329
+ handOffLateApplications(current: () => AgentManager | undefined): void;
4330
+ /**
4331
+ * Hand the outcome of an apply that outlived this manager to the session's current manager
4332
+ * (#540). True when that manager took it: it recorded the outcome, or left the record alone on
4333
+ * purpose. False when there is no such manager or the hand-off failed, and the caller's own late
4334
+ * write is what is left. A failure is logged with its code only, and never fails the apply,
4335
+ * whose pick is on the operator's branch either way.
4336
+ */
4337
+ private handOverLateApplication;
4338
+ /**
4339
+ * Record the outcome of an apply that a disposed manager of this session started (#540).
4340
+ *
4341
+ * Only `applicationStatus` changes, on the record as this manager has it now, so its own later
4342
+ * saves keep it. The record is left alone when it is gone, when its candidate is no longer the
4343
+ * one applied (a re-run replaced it: `applied` would be false of the new one), or when another
4344
+ * live owner holds it (#518, {@link heldByLiveOwner}). A skip is logged rather than thrown,
4345
+ * because the pick is on the branch either way and nobody is waiting on this to fail.
4346
+ *
4347
+ * False when this manager was disposed before it could open its store, and took nothing.
4348
+ */
4349
+ private receiveLateApplication;
4350
+ /**
4351
+ * Whether a live instance other than this one holds the record a late application is for, so
4352
+ * the application must leave it alone. Asked once the record has been read again. The owner it
4353
+ * compares is the one {@link AgentStore.isOwnedByLiveForeign} judges: the writer of the newest
4354
+ * copy the store knows, which is the one on disk unless the store holds a newer one. Two paths:
4355
+ *
4356
+ * - **Main (#544, #568):** an instance of this process without a fresh lease is a disposed one
4357
+ * and owns nothing, and so does one this session has disposed, whatever its lease file says.
4358
+ * That covers the manager handing the pick over, and any manager of this session disposed
4359
+ * after it that wrote the record in between, even when `dispose()` could not remove its lease
4360
+ * (a scanner holding the file) and the lease reads as fresh for up to 15 s.
4361
+ * - **Fallback, the handing-over instance:** it is disposed, whatever its lease file says. This
4362
+ * manager may not know the session's disposed instances: a host can build it by hand rather
4363
+ * than on the session's runtime. A pick landing while that lease reads as fresh would then be
4364
+ * skipped and the record say `not_applied` for good. So a record whose owner is
4365
+ * {@link LateApplication.fromInstanceId} is this manager's to write. No other instance is
4366
+ * exempt: a live owner keeps the record.
4367
+ */
4368
+ private heldByLiveOwner;
4369
+ /**
4370
+ * One record as it is on disk now, read on its own rather than by listing the store: the copy
4371
+ * `get()` would take (this manager has none, another live instance owns it, or it is newer), or
4372
+ * else this manager's own, hydrated. `undefined` when the record is not on disk.
4373
+ */
4374
+ private reloadAgent;
3606
4375
  private pump;
3607
4376
  private evidenceContext;
3608
4377
  private systemPrompt;
@@ -3748,6 +4517,13 @@ interface ToolCall {
3748
4517
  raw: string;
3749
4518
  error: string;
3750
4519
  };
4520
+ /**
4521
+ * Set when the call's arguments, joined across every streamed fragment, were empty or
4522
+ * whitespace on the wire: the name and id arrived and no argument text followed. `arguments`
4523
+ * is `{}` as for a literal `{}`, which this does not mark. The registry rejects such a call
4524
+ * as a route's wire shape when the tool has required arguments, and runs it with `{}` otherwise.
4525
+ */
4526
+ emptyOnWire?: true;
3751
4527
  }
3752
4528
  /** Display-only trace for a tool invoked inside a Task subagent. */
3753
4529
  interface NestedToolInvocation {
@@ -3765,7 +4541,10 @@ interface NestedToolObserver {
3765
4541
  }
3766
4542
  interface FileMutationSummary {
3767
4543
  kind: 'create' | 'update' | 'delete';
4544
+ /** Workspace-relative; a memory file is `memory/<name>.md`, relative to the memory directory. */
3768
4545
  filePath: string;
4546
+ /** The file is in the project memory directory (`ToolContext.memoryWrite`). */
4547
+ memory?: true;
3769
4548
  addedLines: number;
3770
4549
  removedLines: number;
3771
4550
  }
@@ -3894,6 +4673,11 @@ interface ToolDefinition {
3894
4673
  argumentAliases?: Record<string, string>;
3895
4674
  /** Per-item argument aliases for array arguments (argument name → alias → canonical). */
3896
4675
  arrayItemArgumentAliases?: Record<string, Record<string, string>>;
4676
+ /**
4677
+ * Cross-harness arguments with no effect here, dropped before validation rather than refused:
4678
+ * another harness's spelling for something this tool always does (Grep's `-n`).
4679
+ */
4680
+ ignoredArguments?: readonly string[];
3897
4681
  catalog?: ToolCatalogMetadata;
3898
4682
  policy?: ToolPolicy;
3899
4683
  /** When true, the tool is safe to retry once on transient failure (Read, Grep, WebFetch, etc.). */
@@ -3908,18 +4692,20 @@ interface ToolDefinition {
3908
4692
  timeoutMs?: number | ((context: ToolContext) => number | undefined);
3909
4693
  execute: (args: Record<string, unknown>, context: ToolContext) => Promise<ToolResult>;
3910
4694
  }
3911
- interface ReadOnlyRoot {
3912
- root: string;
3913
- /** Subpaths relative to root that must not be read (e.g. ['.inbox']). */
3914
- exclude?: readonly string[];
3915
- }
3916
4695
  interface ToolContext {
3917
4696
  workspaceRoot: string;
3918
4697
  /**
3919
- * Directories outside the workspace that the Read tool may read;
3920
- * today only the project memory directory.
4698
+ * Paths outside the workspace that the Read tool may read: the project memory directory and
4699
+ * the files this session clipped a result into.
4700
+ */
4701
+ readOnlyRoots?: readonly string[];
4702
+ /**
4703
+ * The project memory directory as a *search* root: Glob and Grep reach the files in it (an
4704
+ * absolute pattern or `path`), as Read does through `readOnlyRoots`. Set for every agent that
4705
+ * can read memory, a subagent included. A dot-entry below it (`.format`, a backup) stays out of
4706
+ * a search's results.
3921
4707
  */
3922
- readOnlyRoots?: readonly (string | ReadOnlyRoot)[];
4708
+ memorySearchRoot?: string;
3923
4709
  /**
3924
4710
  * Honored `additionalDirectories`, as resolved real roots the file tools may serve alongside
3925
4711
  * the workspace: Read, Glob and Grep reach them, and so do the write tools' root check. A read
@@ -3928,6 +4714,25 @@ interface ToolContext {
3928
4714
  * managed child and a subagent see the same roots as their parent.
3929
4715
  */
3930
4716
  additionalRoots?: readonly string[];
4717
+ /**
4718
+ * The project memory directory as a *write* root, for the root agent only. Set by the agent loop
4719
+ * when memory is enabled with `autoSave` on, and never copied into a subagent or managed child:
4720
+ * a Task subagent runs in `bypassPermissions`, so the field's absence is what keeps it out of
4721
+ * memory. Write, Edit, MultiEdit and ApplyPatch then reach `*.md` files in `root` (see
4722
+ * `resolveMutationPath`), through the guard in `memory-write-guard.ts`.
4723
+ *
4724
+ * `scope` is `learnings` when another agent drives the session. `sessionId` stamps provenance.
4725
+ */
4726
+ memoryWrite?: {
4727
+ root: string;
4728
+ scope: 'learnings' | 'full';
4729
+ sessionId?: string;
4730
+ };
4731
+ /**
4732
+ * The provider secrets the agent loop redacts from every tool result (#411), longest first.
4733
+ * `BashOutput` reads with them so a chunk never ends inside one. Populated by the agent loop.
4734
+ */
4735
+ providerSecrets?: readonly string[];
3931
4736
  env: Record<string, string>;
3932
4737
  /** Explicit environment overrides safe to persist for opt-in persistent jobs. */
3933
4738
  envOverrides?: Record<string, string>;
@@ -4000,12 +4805,16 @@ interface ToolContext {
4000
4805
  onHookEvent?: (event: string, payload: Record<string, unknown>) => void;
4001
4806
  /** Host sink for one-line informational notices (e.g. memory saved). */
4002
4807
  onNotice?: (notice: string) => void;
4003
- /** Canonical tool names invoked so far in this session. */
4004
- usedToolNames?: Set<string>;
4005
4808
  /** Per-session capability/discovery controller installed by the agent loop. */
4006
4809
  toolDiscovery?: ToolDiscoveryContext;
4007
4810
  /** Mutable resources owned by the current session, separate from configuration. */
4008
4811
  runtime?: SessionRuntime;
4812
+ /** The conversation as the running loop holds it, for a tool that reads it whole (`Advisor`). */
4813
+ conversation?: () => readonly Message[];
4814
+ /** The system prompt of the request the running loop last sent, for the same tool. */
4815
+ systemPrompt?: () => string;
4816
+ /** Spend on a model the tool called itself (`AgentLoopCallbacks.onAuxiliaryUsage`). */
4817
+ onAuxiliaryUsage?: (usage: Usage, metadata: ProviderResponseMetadata, source: string) => void;
4009
4818
  }
4010
4819
  interface ToolSearchMatch {
4011
4820
  name: string;
@@ -4160,6 +4969,9 @@ type AgentEvent = {
4160
4969
  type: 'notice';
4161
4970
  message: string;
4162
4971
  title?: string;
4972
+ } | {
4973
+ type: 'goal_status';
4974
+ event: GoalStatusEvent;
4163
4975
  } | {
4164
4976
  type: 'error';
4165
4977
  error: string;