@bendyline/gezel-service 1.0.4 → 1.0.6

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 (86) hide show
  1. package/dist/bin/gezeld.js +16929 -6105
  2. package/dist/handboek-content/conceptual/connected-apps.md +64 -2
  3. package/dist/handboek-content/conceptual/night-shift.md +22 -2
  4. package/dist/handboek-content/conceptual/welcome.md +2 -0
  5. package/dist/handboek-content/technical/where-files-live.md +3 -1
  6. package/dist/handboek-content/whats-new/1.26226.md +4 -7
  7. package/dist/handboek-content/whats-new/1.26230.md +100 -0
  8. package/dist/handboek.js +19 -9
  9. package/dist/index-store/static-index-worker.js +116 -27
  10. package/dist/index.d.ts +1165 -253
  11. package/dist/index.js +16922 -6098
  12. package/dist/providers/mlx/python/gezel_mlx_server.py +126 -11
  13. package/dist/ui/assets/{TerminalCodeEditor-BuMcfTS4.js → TerminalCodeEditor-BUHpk3pJ.js} +2 -2
  14. package/dist/ui/assets/{_.contribution-CZO2XAKt.js → _.contribution-B34uMeM2.js} +1 -1
  15. package/dist/ui/assets/{chunk-2JJ5RFDZ-Ckbzd10a.js → chunk-2JJ5RFDZ-B2-BFNXu.js} +1 -1
  16. package/dist/ui/assets/{chunk-BAOV476U-yPHu-ECL.js → chunk-BAOV476U-CEOFYt8X.js} +1 -1
  17. package/dist/ui/assets/{chunk-GAZKTT4R-BUf32614.js → chunk-GAZKTT4R-BkP6-w3z.js} +1 -1
  18. package/dist/ui/assets/{chunk-USU6HTKB-B-ElCUQS.js → chunk-USU6HTKB-htGRNErW.js} +1 -1
  19. package/dist/ui/assets/{cpp.contribution-BJzhk0Zj.js → cpp.contribution-CvG4GFxL.js} +1 -1
  20. package/dist/ui/assets/{csharp.contribution-BR9K2qKj.js → csharp.contribution-DekWAPj3.js} +1 -1
  21. package/dist/ui/assets/{css.contribution-CfkVmdah.js → css.contribution-CDuF4Xjo.js} +1 -1
  22. package/dist/ui/assets/{cssMode-MMeGUmV2.js → cssMode-D848zTHU.js} +1 -1
  23. package/dist/ui/assets/{csv-RAgECoUg.js → csv-PublMwG8.js} +1 -1
  24. package/dist/ui/assets/dist-CJRsJTdk.js +222 -0
  25. package/dist/ui/assets/dist-Cg3Ab49q.js +2 -0
  26. package/dist/ui/assets/{doc-B7NXSLxH.js → doc-Bq50ED-X.js} +41 -41
  27. package/dist/ui/assets/{dockerfile.contribution-IZMl57B5.js → dockerfile.contribution-ChT95Kbh.js} +1 -1
  28. package/dist/ui/assets/{docx-BEJpkI6I.js → docx-BSDeP5Eu.js} +1 -1
  29. package/dist/ui/assets/{epub-00L2M424.js → epub-D8zTSe9-.js} +1 -1
  30. package/dist/ui/assets/{go.contribution-BxCBwhZs.js → go.contribution-D_1myt_B.js} +1 -1
  31. package/dist/ui/assets/{handlebars-CreIkams.js → handlebars-Mxcy9PWv.js} +1 -1
  32. package/dist/ui/assets/{handlebars.contribution-BGlyL-dm.js → handlebars.contribution-DYF5z0XX.js} +2 -2
  33. package/dist/ui/assets/{html-BSrz1IKi.js → html-HXzM-WGZ.js} +1 -1
  34. package/dist/ui/assets/{html-Cgejr9YM.js → html-X72kRvXT.js} +1 -1
  35. package/dist/ui/assets/{html.contribution-CucOMFQ_.js → html.contribution-C0xFTPDL.js} +2 -2
  36. package/dist/ui/assets/{htmlMode-DRG7XY5Z.js → htmlMode-BVtBWJQM.js} +1 -1
  37. package/dist/ui/assets/{index-Bqu-32S3.js → index-DUf4wIj5.js} +207 -200
  38. package/dist/ui/assets/index-udlcniNf.css +1 -0
  39. package/dist/ui/assets/{infer-D6e75m-s.js → infer-Be4Xn3O1.js} +1 -1
  40. package/dist/ui/assets/{ini.contribution-CGX5OdXM.js → ini.contribution-BqUQ8Oa0.js} +1 -1
  41. package/dist/ui/assets/{java.contribution-CVhfJ_us.js → java.contribution-u4QvI2UZ.js} +1 -1
  42. package/dist/ui/assets/{javascript-QelFp9t5.js → javascript-BVm8fvMk.js} +1 -1
  43. package/dist/ui/assets/javascript.contribution-BtlENuoy.js +2 -0
  44. package/dist/ui/assets/{jsonMode-T4Ss3lDu.js → jsonMode-DcoSB4Jg.js} +1 -1
  45. package/dist/ui/assets/{kotlin.contribution-DxFbGDYA.js → kotlin.contribution-D1ZfW5lW.js} +1 -1
  46. package/dist/ui/assets/{less.contribution-g9R8XRZf.js → less.contribution-BBWek5Gc.js} +1 -1
  47. package/dist/ui/assets/{lua.contribution-YqDsEy8Z.js → lua.contribution-DJEpMduq.js} +1 -1
  48. package/dist/ui/assets/mapTheme-4TWH25FT-C2uLgUhs.js +1 -0
  49. package/dist/ui/assets/{markdown.contribution-DboXnPnx.js → markdown.contribution-CsVCTE0K.js} +1 -1
  50. package/dist/ui/assets/{monaco-BdFw56DI.js → monaco-DcZAEsIZ.js} +3 -3
  51. package/dist/ui/assets/{monaco-base-DrP3D13F.js → monaco-base-C-UMfHa9.js} +1 -1
  52. package/dist/ui/assets/{monaco-setup-QyF22y9h.js → monaco-setup-Dy_kv6Gv.js} +1 -1
  53. package/dist/ui/assets/{monaco.contribution-FA_ESP-W.js → monaco.contribution-BpJqoa65.js} +2 -2
  54. package/dist/ui/assets/{monaco.contribution-XEpc63ty.js → monaco.contribution-BujOhotZ.js} +2 -2
  55. package/dist/ui/assets/{monaco.contribution-BkqIxESv.js → monaco.contribution-DW4-UIJp.js} +2 -2
  56. package/dist/ui/assets/{monaco.contribution-CMzPFHtO.js → monaco.contribution-ypbCAEx4.js} +2 -2
  57. package/dist/ui/assets/{pdf-66KslaXf.js → pdf-BZeBurJI.js} +1 -1
  58. package/dist/ui/assets/{php.contribution-rnpik0Kk.js → php.contribution-B6Atm_xI.js} +1 -1
  59. package/dist/ui/assets/{pptx-DZdEoDP9.js → pptx-BbAcnlML.js} +4 -4
  60. package/dist/ui/assets/{python-CF7X5t9Q.js → python-CCiWacbd.js} +1 -1
  61. package/dist/ui/assets/{python.contribution-CyJGCfSq.js → python.contribution-3CThbaoI.js} +2 -2
  62. package/dist/ui/assets/{ruby.contribution-4ZyPGu2E.js → ruby.contribution-D3g6EADN.js} +1 -1
  63. package/dist/ui/assets/{rust.contribution-Cl_B3X2G.js → rust.contribution-CSdjIX0R.js} +1 -1
  64. package/dist/ui/assets/{schemas-Drh23Boe.js → schemas-JY4jmLbC.js} +1 -1
  65. package/dist/ui/assets/{scss.contribution-BO02gVLZ.js → scss.contribution-mQ_vuYxE.js} +1 -1
  66. package/dist/ui/assets/{shell.contribution-Dn9Y0uHs.js → shell.contribution-BLQSLx3H.js} +1 -1
  67. package/dist/ui/assets/{sql.contribution-Bog_5Rc8.js → sql.contribution-GScL7Mrt.js} +1 -1
  68. package/dist/ui/assets/{swift.contribution-CfiAr7AX.js → swift.contribution-DG__yqNr.js} +1 -1
  69. package/dist/ui/assets/{terminal-monaco-setup-B-EnkGiy.js → terminal-monaco-setup-x6piH1Un.js} +1 -1
  70. package/dist/ui/assets/{transform-MBfVXq54.js → transform-ywncQECB.js} +1 -1
  71. package/dist/ui/assets/{tsMode-B2WuLuPL.js → tsMode-B5nM5MR7.js} +1 -1
  72. package/dist/ui/assets/{typescript-Bv_J1tKT.js → typescript-xiOVha1M.js} +1 -1
  73. package/dist/ui/assets/{typescript.contribution-CM0XQCYi.js → typescript.contribution-BU2V4Vz8.js} +2 -2
  74. package/dist/ui/assets/worker-BzdDEeh7.js +1 -0
  75. package/dist/ui/assets/{xlsx-DDBRN-IU.js → xlsx-f9AYHB2W.js} +1 -1
  76. package/dist/ui/assets/{xml-DMV-f4tm.js → xml-m3MSnvuD.js} +1 -1
  77. package/dist/ui/assets/{xml.contribution-QqmOB9gT.js → xml.contribution-bOoaS55N.js} +2 -2
  78. package/dist/ui/assets/{yaml-Bb6mZXY5.js → yaml-DOtMVgbb.js} +1 -1
  79. package/dist/ui/assets/{yaml.contribution-DCDnWhHR.js → yaml.contribution-CNCYEtwk.js} +2 -2
  80. package/dist/ui/index.html +14 -14
  81. package/package.json +13 -12
  82. package/dist/ui/assets/dist-8uH4DcHx.js +0 -2
  83. package/dist/ui/assets/dist-CFOmf7nD.js +0 -222
  84. package/dist/ui/assets/index-DTicLc4o.css +0 -1
  85. package/dist/ui/assets/javascript.contribution--no0ijCu.js +0 -2
  86. package/dist/ui/assets/mapTheme-4TWH25FT-B5nAKRmb.js +0 -1
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _bendyline_gezel from '@bendyline/gezel';
2
- import { HistoryEvent, HistoryFilter, HistoryEntry, GezelGender, Poppetje, ProjectFileEntry, GezelDetail, GezelConfig, GezelSummary, ProviderName, ChatModelTuning, GezelTrait, ProjectLocalConfig, ImportProvenance, PendingImports, GezelFrontmatter, Project, ProjectDetail, FileReviewIssue, BoekwachterIssueStatus, BoekwachterIssueDismissalReason, ProjectManagedWorkspaceWritePolicy, ProjectNudgeConfig, ProjectTabVisibility, ProjectActivity, MeesterStatusReport, MeesterStatusState, ProjectGitHub, ChatSession, ChatSessionSummary, TimelineMessage, TerminalTimelineEntry, TerminalThread, TerminalThreadSummary, TerminalMessage, GezelGrowthState, Task, Question, TaskNote, ToolsetsScope, InstalledToolset, ToolsetConfig, CraftbookSummary, Craftbook, ProjectCraftbookProvenance, ChannelStatus, ChannelName, ChatEvent, ChatEventEnvelope, HookSpec, HookPhase, HookResult, ModelTier, ModelStyle, BehaviorId, ModelFamily, ChatMessageToolCall, SamplingBlock, CodexPermissionModeCompat, ModelFitnessTrigger, ModelFitnessRecord, OutlineFileResponse, FileContextResponse, FindSymbolResponse, ReadSymbolResponse, MapRepoResponse, SecurityScanResponse, ScanFindingsRequest, ScanFindingsResponse, SecurityFindingWire, MapAttackSurfaceResponse, ListDependenciesResponse, SecurityOverviewResponse, TraceTaintResponse, FileMapRequest, FileMapResponse, SearchCodeResponse, FileReviewResponse, ListFileIssuesRequest, ListFileIssuesResponse, BoekwachterIssue, SearchDocsResponse, ReadDocAsMarkdownResponse, SearchImagesResponse, FindSimilarImagesResponse, DescribeFolderResponse, FindEntityResponse, ListEntityMentionsResponse, KeurmeesterCaseRecord, TaskStatus, KeurmeesterTriggerKind, KeurmeesterAction, KeurmeesterCaseOutcome, GateCheck, TaskCraftbookStep, TaskCraftbookSource, CraftbookConnectorNeed, CreateTaskRequest, UpdateTaskRequest, TaskAssignee, NewCraftbookStep, StepPosition, UpdateTaskStepRequest, TaskNoteAuthor, TaskVariation, ScriptCapability, ScriptScope, ScriptRunTrigger, ScriptRun, NativeEngineResolveEvent, NativeEngineName, ImageRecognition, RecognitionMode, RecognitionHealth, PreviewLogEntry, SessionGpuTask, SessionTelemetry, AIEngagementMode, ChatMessage, SessionDebugSnapshot, ExpectedDeliverable, CodexSetupStatusResponse, ConfigureCodexRequest, ProjectConnectorBinding, GildeUpdateStatusResponse, GitWorkingChange, GitFileDiffResponse, CodeReviewKind, GitLogEntry, GitCommitDetailResponse, GitSyncResponse, GitConflictFile, GitConflictVersionsResponse, WorkspaceIndexStatus, WorkspaceCommandIndex, WorkspaceSkillIndex, WorkspaceInstructionIndex, CodeReviewRecord, CodeReview, GitHubPullSummary, GitHubPullDetail, GitHubPullFile, GitHubPullComment, ImageModelPullEvent as ImageModelPullEvent$1, VideoAccelerator, RenderImageRequest, ReportActionsResponse, FireReportActionResponse, ReportActionRecord, UnifiedSearchResult, SystemToolsetInstallEvent, SystemToolsetInstallSnapshot, NightShiftWindow, TerminalEventEnvelope, DiscoveredCommand, ServiceRole, ScriptMeta } from '@bendyline/gezel';
2
+ import { ChatSessionSource, ChatEvent, ChatEventEnvelope, HistoryEvent, HistoryFilter, HistoryEntry, ProjectFileEntry, GezelGender, Poppetje, GezelDetail, GezelConfig, GezelSummary, ProviderName, ChatModelTuning, GezelTrait, ProjectLocalConfig, ImportProvenance, PendingImports, GezelFrontmatter, Project, ProjectIconId, ProjectDetail, FileReviewIssue, BoekwachterIssueStatus, BoekwachterIssueDismissalReason, ProjectManagedWorkspaceWritePolicy, ProjectNudgeConfig, ProjectTabVisibility, ProjectActivity, MeesterStatusReport, MeesterStatusState, ProjectGitHub, ChatSession, ChatSessionSummary, TimelineMessage, TerminalTimelineEntry, TerminalThread, TerminalThreadSummary, TerminalMessage, GezelGrowthState, Task, Question, TaskNote, ToolsetsScope, InstalledToolset, ToolsetConfig, CraftbookSummary, Craftbook, ProjectCraftbookProvenance, AmbientDashboardResolution, AmbientDashboardTheme, AmbientDashboardDisplayTarget, AmbientDashboardStyle, AmbientDashboardState, ChannelStatus, ChannelName, HookSpec, HookPhase, HookResult, ModelTier, ModelStyle, BehaviorId, ModelFamily, ChatMessageToolCall, SamplingBlock, CodexPermissionModeCompat, ModelFitnessTrigger, ModelFitnessRecord, OutlineFileResponse, FileContextResponse, FindSymbolResponse, ReadSymbolResponse, MapRepoResponse, SecurityScanResponse, ScanFindingsRequest, ScanFindingsResponse, SecurityFindingWire, MapAttackSurfaceResponse, ListDependenciesResponse, SecurityOverviewResponse, TraceTaintResponse, FileMapRequest, FileMapResponse, SearchCodeResponse, FileReviewResponse, ListFileIssuesRequest, ListFileIssuesResponse, BoekwachterIssue, SearchDocsResponse, SearchDocumentsResponse, ReadDocAsMarkdownResponse, SearchImagesResponse, FindSimilarImagesResponse, DescribeFolderResponse, FindEntityResponse, ListEntityMentionsResponse, KeurmeesterCaseRecord, TaskStatus, KeurmeesterTriggerKind, KeurmeesterAction, KeurmeesterCaseOutcome, GateCheck, TaskCraftbookStep, TaskCraftbookSource, CraftbookConnectorNeed, CreateTaskRequest, UpdateTaskRequest, TaskAssignee, NewCraftbookStep, StepPosition, UpdateTaskStepRequest, TaskNoteAuthor, TaskVariation, ScriptCapability, ScriptScope, ScriptRunTrigger, ScriptRun, NativeEngineResolveEvent, NativeEngineName, ImageRecognition, RecognitionMode, RecognitionHealth, PreviewLogEntry, SessionGpuTask, SessionTelemetry, AIEngagementMode, ChatMessage, SessionDebugSnapshot, ExpectedDeliverable, CodexSetupStatusResponse, ConfigureCodexRequest, ProjectConnectorBinding, GildeUpdateStatusResponse, GitWorkingChange, GitFileDiffResponse, CodeReviewKind, GitLogEntry, GitCommitDetailResponse, GitSyncResponse, GitConflictFile, GitConflictVersionsResponse, WorkspaceIndexStatus, WorkspaceCommandIndex, WorkspaceSkillIndex, WorkspaceInstructionIndex, CodeReviewRecord, CodeReview, GitHubPullSummary, GitHubPullDetail, GitHubPullFile, GitHubPullComment, OpenCodeSetupStatusResponse, ConfigureOpenCodeRequest, InstallOpenCodePluginRequest, PiSetupStatusResponse, ConfigurePiRequest, InstallPiExtensionRequest, ImageModelPullEvent as ImageModelPullEvent$1, VideoAccelerator, RenderImageRequest, ReportActionsResponse, FireReportActionResponse, ReportActionRecord, UnifiedSearchResult, StorageJobKind, StorageJob, StorageJobPhase, SystemToolsetInstallEvent, SystemToolsetInstallSnapshot, NightShiftWindow, TerminalEventEnvelope, DiscoveredCommand, VSCodeSetupStatusResponse, ConfigureVSCodeRequest, ServiceRole, ScriptMeta } from '@bendyline/gezel';
3
3
  import { ServerType } from '@hono/node-server';
4
4
  import { CatalogService } from '@bendyline/gezel-catalog';
5
5
  import { ExternalFolders } from '@bendyline/gezel/paths';
@@ -38,21 +38,97 @@ interface LoopbackCert {
38
38
  fingerprintBase64: string;
39
39
  }
40
40
 
41
+ type SessionListener = (event: ChatEvent) => void;
42
+ type EnvelopeListener = (envelope: ChatEventEnvelope) => void;
43
+ interface PublishScope {
44
+ sessionId: string;
45
+ gezelId: string;
46
+ projectId: string;
47
+ sessionSource?: ChatSessionSource;
48
+ }
41
49
  /**
42
- * Process-wide debug flag.
50
+ * Per-session chat event bus, plus project-scoped and global fan-out for the
51
+ * interleaved timeline UI. Each `publish` reaches three subscriber sets:
43
52
  *
44
- * Subsystems hold a shared reference and read `isEnabled()` at the
45
- * point of logging so a config flip via `PUT /api/config` —
46
- * propagates instantly without re-spawning anything. The flag never
47
- * touches disk itself; the source of truth is `GezelConfig.debugMode`
48
- * in `~/.gezel/config.json`, re-read on service boot and on every
49
- * config PUT.
53
+ * 1. Listeners on this session bare `ChatEvent`. Get the recent history
54
+ * replayed when they subscribe (so a late session-scoped subscriber can
55
+ * catch up on the in-flight message). Cleared on `done`.
56
+ * 2. Listeners on this project `ChatEventEnvelope` with sessionId/
57
+ * gezelId/projectId. Also get the history replayed for any in-flight
58
+ * sessions in this project, so a user who tabs away mid-stream and back
59
+ * doesn't lose the partial text + tools accumulated during the gap.
60
+ * 3. Global listeners — `ChatEventEnvelope`. Same replay semantics across
61
+ * every in-flight session anywhere.
62
+ *
63
+ * History is bus-side only: cleared on `done` so completed turns don't get
64
+ * re-delivered when a new subscriber connects after the fact.
65
+ *
66
+ * If a send truly cannot resolve its session record, it does not know the
67
+ * gezelId/projectId and publishes session-scoped via `publishSessionOnly`.
68
+ * Once a record is known, even provider-start failures use `publish` so
69
+ * project/global clients receive the error.
50
70
  */
51
- declare class DebugFlag {
52
- private _enabled;
53
- constructor(initial?: boolean);
54
- isEnabled(): boolean;
55
- set(next: boolean): void;
71
+ declare class ChatEventBus {
72
+ private readonly sessionBuses;
73
+ private readonly projectListeners;
74
+ private readonly gezelListeners;
75
+ private readonly globalListeners;
76
+ publish(scope: PublishScope, event: ChatEvent): void;
77
+ /**
78
+ * Publish to the session bus only. Used when the manager's early-fail path
79
+ * doesn't know the gezel/project to envelope (the session record itself
80
+ * couldn't be loaded). Project + global subscribers never see these.
81
+ */
82
+ publishSessionOnly(sessionId: string, event: ChatEvent): void;
83
+ /**
84
+ * Publish a project-scoped lifecycle event that has no originating
85
+ * turn — e.g. `project_created`. Fans out to the project's listeners
86
+ * and all global listeners, but is deliberately NOT recorded in any
87
+ * session history: these are one-shot signals, not part of a
88
+ * replayable transcript, so a subscriber that reconnects later must
89
+ * not see them re-fired (unlike `publish`, which seeds `deliverToSession`
90
+ * history that replays until the turn's `done`). `sessionId`/`gezelId`
91
+ * are empty — the consumers that care (the sidebar refresh bridge)
92
+ * key off the event payload, not the envelope scope.
93
+ */
94
+ publishProjectEvent(projectId: string, event: ChatEvent): void;
95
+ /**
96
+ * Publish a project-less global lifecycle event — e.g. `night_shift`.
97
+ * Reaches only the global listeners (the Meester timeline SSE the UI
98
+ * subscribes to via `subscribeAll`); like `publishProjectEvent` it is
99
+ * not recorded in any session history, so reconnecting subscribers do
100
+ * not see it re-fired. The envelope scope fields are all empty;
101
+ * consumers key off the event payload.
102
+ */
103
+ publishGlobalEvent(event: ChatEvent): void;
104
+ subscribe(sessionId: string, listener: SessionListener): () => void;
105
+ subscribeProject(projectId: string, listener: EnvelopeListener): () => void;
106
+ subscribeGezel(gezelId: string, listener: EnvelopeListener): () => void;
107
+ subscribeAll(listener: EnvelopeListener): () => void;
108
+ private deliverToSession;
109
+ /**
110
+ * Append an event to a session's replay history, keeping the buffer
111
+ * lossless for the content that matters. Two rules:
112
+ *
113
+ * 1. Transient status pulses (`heartbeat`, `wire_pulse`) are delivered
114
+ * live but never recorded — they carry no replayable content, and a
115
+ * reasoning-heavy local-model turn emits enough of them to evict
116
+ * real deltas out of the 500-event cap.
117
+ * 2. Adjacent `delta` events coalesce into one entry. A five-minute
118
+ * thinking turn streams thousands of per-token deltas; stored
119
+ * individually they blow past the cap and the earliest thinking is
120
+ * silently dropped on replay. Merged, a text run costs one slot no
121
+ * matter how long it streamed. The stored entry is a fresh object —
122
+ * the original event was already handed to live listeners and must
123
+ * not be mutated.
124
+ */
125
+ private recordInHistory;
126
+ private ensureSession;
127
+ private cleanupSession;
128
+ private pruneIdleSessions;
129
+ private safeDeliver;
130
+ /** Lightweight observability hook used by tests and diagnostics. */
131
+ activeSessionCount(): number;
56
132
  }
57
133
 
58
134
  /**
@@ -123,6 +199,18 @@ declare class HistoryManager {
123
199
  private sessionMatchesQuery;
124
200
  }
125
201
 
202
+ interface WalkDirResult {
203
+ entries: ProjectFileEntry[];
204
+ /** True when `maxEntries` or `maxDepth` dropped part of the tree. */
205
+ truncated: boolean;
206
+ }
207
+
208
+ /** Who is writing. Absent means the person, editing in the app. */
209
+ interface DocumentWriteActor {
210
+ gezelId?: string;
211
+ sessionId?: string;
212
+ }
213
+
126
214
  /**
127
215
  * Shared format + parser for the daily memory markdown files
128
216
  * (`memories/daily/YYYY-MM-DD.md`). These files are the memory system's
@@ -224,12 +312,6 @@ interface JournalContext {
224
312
  sessionId?: string;
225
313
  }
226
314
 
227
- interface WalkDirResult {
228
- entries: ProjectFileEntry[];
229
- /** True when `maxEntries` or `maxDepth` dropped part of the tree. */
230
- truncated: boolean;
231
- }
232
-
233
315
  type ProjectArtifactResolveResult = {
234
316
  kind: 'found';
235
317
  content: string;
@@ -327,6 +409,11 @@ interface StoreOptions {
327
409
  * history can construct a bare Store.
328
410
  */
329
411
  history?: HistoryManager;
412
+ /**
413
+ * Test seam: shorten the document-edit audit's quiet window so a test can
414
+ * observe a sitting close without waiting minutes.
415
+ */
416
+ auditQuietWindowMs?: number;
330
417
  /**
331
418
  * Per-scope external roots, captured at boot from
332
419
  * `config.json#externalFolders`. When set, the Store routes reads/
@@ -365,7 +452,8 @@ interface SessionChangeEvent {
365
452
  sessionId: string;
366
453
  }
367
454
  interface DocumentChangeEvent {
368
- type: 'write' | 'delete';
455
+ /** `mkdir` carries no content — listeners that index files skip it. */
456
+ type: 'write' | 'delete' | 'mkdir';
369
457
  path: string;
370
458
  }
371
459
  interface GezelChangeEvent {
@@ -396,8 +484,8 @@ declare class Store {
396
484
  * enqueue-only and never throw work back into the write path.
397
485
  */
398
486
  private readonly sessionListeners;
399
- /** Notified on every document write/delete, including overwrites (which
400
- * emit no history event). Same enqueue-only contract as session listeners. */
487
+ /** Notified on every document write/delete, including overwrites. Same
488
+ * enqueue-only contract as session listeners. */
401
489
  private readonly documentListeners;
402
490
  /** Notified after a shared gezel has been fully created and is listable.
403
491
  * Project-local gezels use a separate Store path and intentionally do not
@@ -535,6 +623,37 @@ declare class Store {
535
623
  */
536
624
  private migrateLegacyTemperatureField;
537
625
  ensureDefaultProject(): Promise<void>;
626
+ /**
627
+ * Ensure the canonical `shared` project exists and points at the resolved
628
+ * documents root. Its workspace IS the shared document library, so every
629
+ * per-project service (content index, shadow conversion, enrichment,
630
+ * watcher, search) reaches the library without a parallel pipeline.
631
+ *
632
+ * The library folder is user-owned and possibly cloud-synced, so nothing
633
+ * here writes into it beyond the one starter document on a first run.
634
+ *
635
+ * Returns whether it created the project, so boot can do the one-shot
636
+ * roster wiring (Boekwachter) exactly once.
637
+ */
638
+ ensureSharedProject(): Promise<{
639
+ id: string;
640
+ created: boolean;
641
+ }>;
642
+ /**
643
+ * Id of the shared library project, or null when it does not exist yet
644
+ * (machine-engine role, or a boot that has not reached the ensure).
645
+ * Callers that need the library's index — document search, recall — go
646
+ * through this rather than assuming the default id.
647
+ */
648
+ sharedProjectId(): Promise<string | null>;
649
+ private createSharedProject;
650
+ /**
651
+ * One starter document, and only when the library is empty — an explanation
652
+ * a first-run user can read and edit, not a file we re-create after they
653
+ * delete it.
654
+ */
655
+ private seedSharedLibraryStarterDoc;
656
+ private backfillSharedProjectDocs;
538
657
  /**
539
658
  * Seed the curated catch-all docs on installs whose `default` project
540
659
  * predates them. Only writes a doc that is genuinely absent — an existing
@@ -545,9 +664,18 @@ declare class Store {
545
664
  /**
546
665
  * Ensure `config.meesterGezelId` points at a valid gezel. Order of preference:
547
666
  * 1. Existing pointer → a live gezel: no-op.
548
- * 2. Any other gezels exist: auto-pick the first alphabetically.
549
- * 3. No gezels at all: create a fresh Meester with a random name and the
667
+ * 2. A gezel whose role is already Meester: designate it.
668
+ * 3. Any other gezels exist: auto-pick the first alphabetically.
669
+ * 4. No gezels at all: create a fresh Meester with a random name and the
550
670
  * curated about.md from packages/service/src/meester/prompt.ts.
671
+ *
672
+ * Step 2 exists because the roster is sorted by name, so taking the first
673
+ * entry crowned whoever sorted earliest — a Klerk named Ilse ahead of a real
674
+ * Meester named Ulrike. The badge is only half the job: a Meester's power is
675
+ * its curated about.md teaching the team-management tools, which a Klerk
676
+ * does not have, so the front door silently lost its abilities. Reachable
677
+ * whenever the pointer goes stale (a deleted gezel, a restored backup) and
678
+ * on any install that mounts a machine-shared roster.
551
679
  */
552
680
  ensureDefaultMeester(): Promise<void>;
553
681
  /**
@@ -835,6 +963,8 @@ declare class Store {
835
963
  createProject(input: {
836
964
  name: string;
837
965
  description?: string;
966
+ /** Explicit maker's-mark override; missing inherits from the project type. */
967
+ icon?: ProjectIconId;
838
968
  /** Written to documents/about.md at creation. */
839
969
  about?: string;
840
970
  /** Written to documents/missionObjectives.md at creation. */
@@ -916,6 +1046,8 @@ declare class Store {
916
1046
  updateProject(id: string, patch: {
917
1047
  name?: string;
918
1048
  description?: string;
1049
+ /** Explicit maker's-mark override; null resumes type inheritance. */
1050
+ icon?: ProjectIconId | null;
919
1051
  workingDir?: string | null;
920
1052
  voormanGezelId?: string | null;
921
1053
  voormanAutoAssignedAt?: string;
@@ -1071,14 +1203,21 @@ declare class Store {
1071
1203
  writeProjectDoc(id: string, name: string, content: string): Promise<void>;
1072
1204
  private tryGetProjectMeta;
1073
1205
  projectArtifactsDir(id: string): string;
1074
- listProjectArtifacts(id: string, subpath?: string): Promise<ProjectFileEntry[]>;
1206
+ listProjectArtifacts(id: string, subpath?: string, opts?: {
1207
+ includeHidden?: boolean;
1208
+ }): Promise<ProjectFileEntry[]>;
1075
1209
  listProjectArtifactsRecursive(id: string, opts?: {
1076
1210
  withStats?: boolean;
1211
+ includeHidden?: boolean;
1212
+ subpath?: string;
1077
1213
  }): Promise<ProjectFileEntry[]>;
1078
1214
  listProjectArtifactsRecursiveDetailed(id: string, opts?: {
1079
1215
  withStats?: boolean;
1216
+ includeHidden?: boolean;
1217
+ subpath?: string;
1080
1218
  }): Promise<WalkDirResult>;
1081
1219
  readProjectArtifact(id: string, filePath: string): Promise<string | null>;
1220
+ projectArtifactSize(id: string, filePath: string): Promise<number | null>;
1082
1221
  /**
1083
1222
  * Read an artifact as raw bytes plus a MIME type guess (from the file
1084
1223
  * extension). Used by binary consumers — e.g. the image-layer resolver
@@ -1166,6 +1305,11 @@ declare class Store {
1166
1305
  createOnly?: boolean;
1167
1306
  }): Promise<string>;
1168
1307
  deleteProjectArtifact(id: string, filePath: string): Promise<void>;
1308
+ createProjectArtifactFolder(id: string, folderPath: string): Promise<string>;
1309
+ renameProjectArtifactPath(id: string, fromPath: string, toPath: string): Promise<{
1310
+ fromPath: string;
1311
+ toPath: string;
1312
+ }>;
1169
1313
  private sessionImagesDir;
1170
1314
  /** Relative path used inside chat-message markdown, e.g. "images/abc.png". */
1171
1315
  private sessionImageRelPath;
@@ -1224,11 +1368,28 @@ declare class Store {
1224
1368
  */
1225
1369
  private resolveWorkspaceDir;
1226
1370
  projectWorkspaceDir(id: string): Promise<string>;
1227
- listProjectWorkspace(id: string, subpath?: string): Promise<ProjectFileEntry[]>;
1228
- listProjectWorkspaceRecursive(id: string): Promise<ProjectFileEntry[]>;
1229
- /** Recursive listing plus the truncation flag, for surfaces that must
1230
- * tell the user/model when the walker's entry cap dropped files. */
1231
- listProjectWorkspaceRecursiveDetailed(id: string): Promise<WalkDirResult>;
1371
+ listProjectWorkspace(id: string, subpath?: string, opts?: {
1372
+ includeHidden?: boolean;
1373
+ }): Promise<ProjectFileEntry[]>;
1374
+ listProjectWorkspaceRecursive(id: string, opts?: {
1375
+ withStats?: boolean;
1376
+ includeHidden?: boolean;
1377
+ subpath?: string;
1378
+ }): Promise<ProjectFileEntry[]>;
1379
+ /**
1380
+ * Recursive listing plus the truncation flag, for surfaces that must tell
1381
+ * the user/model when the walker's entry cap dropped files.
1382
+ *
1383
+ * `subpath` roots the walk instead of post-filtering it, so a caller asking
1384
+ * for one folder is not competing with the whole workspace for the entry
1385
+ * budget — the same contract as the artifacts twin. Entry paths stay
1386
+ * workspace-root-relative, so results remain directly readable.
1387
+ */
1388
+ listProjectWorkspaceRecursiveDetailed(id: string, opts?: {
1389
+ withStats?: boolean;
1390
+ includeHidden?: boolean;
1391
+ subpath?: string;
1392
+ }): Promise<WalkDirResult>;
1232
1393
  listProjectWorkspaceHtmlPages(id: string): Promise<ProjectFileEntry[]>;
1233
1394
  readProjectWorkspaceFile(id: string, filePath: string): Promise<string | null>;
1234
1395
  /**
@@ -1397,7 +1558,18 @@ declare class Store {
1397
1558
  documentsDir(): string;
1398
1559
  listDocuments(subpath?: string): Promise<ProjectFileEntry[]>;
1399
1560
  listDocumentsRecursive(): Promise<ProjectFileEntry[]>;
1561
+ /** Close any in-flight document-edit audit windows (service shutdown). */
1562
+ flushDocumentAudit(): Promise<void>;
1563
+ readDocumentAsMarkdown(filePath: string): Promise<{
1564
+ content: string;
1565
+ converted: boolean;
1566
+ } | null>;
1567
+ listDocumentsRecursiveDetailed(opts?: {
1568
+ withStats?: boolean;
1569
+ includeHidden?: boolean;
1570
+ }): Promise<WalkDirResult>;
1400
1571
  readDocument(filePath: string): Promise<string | null>;
1572
+ documentSize(filePath: string): Promise<number | null>;
1401
1573
  /**
1402
1574
  * Read a shared-library document sidecar as raw bytes. Native document
1403
1575
  * media export uses this for images, narration, and embedded clips while
@@ -1407,7 +1579,7 @@ declare class Store {
1407
1579
  data: Buffer;
1408
1580
  mimeType: string;
1409
1581
  } | null>;
1410
- writeDocument(filePath: string, content: string): Promise<void>;
1582
+ writeDocument(filePath: string, content: string, actor?: DocumentWriteActor): Promise<void>;
1411
1583
  /**
1412
1584
  * Binary sibling of `writeDocument` — same dir, same path-traversal
1413
1585
  * guard, but writes raw bytes. Powers the squisq editor's Files panel
@@ -1415,7 +1587,7 @@ declare class Store {
1415
1587
  * document.
1416
1588
  */
1417
1589
  writeDocumentBinary(filePath: string, data: Uint8Array): Promise<void>;
1418
- deleteDocument(filePath: string): Promise<void>;
1590
+ deleteDocument(filePath: string, actor?: DocumentWriteActor): Promise<void>;
1419
1591
  createDocumentFolder(folderPath: string): Promise<void>;
1420
1592
  renameDocument(fromPath: string, toPath: string): Promise<void>;
1421
1593
  writeSession(session: ChatSession): Promise<void>;
@@ -1477,6 +1649,13 @@ declare class Store {
1477
1649
  limit: number;
1478
1650
  before?: string;
1479
1651
  includeArchived?: boolean;
1652
+ /**
1653
+ * Indexed workspace paths for a project, used to recognize workspace
1654
+ * files an assistant reply named in prose. Supplied by the caller
1655
+ * because the listing's owner (`WorkspaceIndexManager`) is built on top
1656
+ * of this Store. Omit it and the backfill sees artifacts only.
1657
+ */
1658
+ workspaceFiles?: (projectId: string) => Promise<readonly string[]>;
1480
1659
  }): Promise<{
1481
1660
  messages: TimelineMessage[];
1482
1661
  hasMore: boolean;
@@ -1695,6 +1874,156 @@ declare class Store {
1695
1874
  writeProjectCraftbookProvenance(projectId: string, id: string, prov: ProjectCraftbookProvenance): Promise<void>;
1696
1875
  }
1697
1876
 
1877
+ interface ActivityTrackerOptions {
1878
+ store: Store;
1879
+ history: HistoryManager;
1880
+ chatEvents: ChatEventBus;
1881
+ now?: () => number;
1882
+ persistThresholdMs?: number;
1883
+ }
1884
+ declare class ActivityTracker {
1885
+ private readonly store;
1886
+ private readonly history;
1887
+ private readonly chatEvents;
1888
+ private readonly now;
1889
+ private readonly persistThresholdMs;
1890
+ /** Newest observed stamp per project (ms epoch). */
1891
+ private readonly latest;
1892
+ /** Stamp last written to (or read from) disk per project. */
1893
+ private readonly persisted;
1894
+ /** Projects whose on-disk stamp has been loaded into `persisted`. */
1895
+ private readonly loads;
1896
+ private readonly writing;
1897
+ private unsubs;
1898
+ constructor(opts: ActivityTrackerOptions);
1899
+ start(): void;
1900
+ stop(): Promise<void>;
1901
+ stamp(projectId: string, atMs: number): void;
1902
+ /**
1903
+ * Newest known activity for a project — max of the in-memory stamp
1904
+ * and the on-disk one (loaded once). Null when the project has never
1905
+ * been observed.
1906
+ */
1907
+ lastActivityAt(projectId: string): Promise<string | null>;
1908
+ /** Persist every stamp that is newer than its on-disk value. */
1909
+ flush(): Promise<void>;
1910
+ private maybePersist;
1911
+ private write;
1912
+ private ensureLoaded;
1913
+ }
1914
+
1915
+ /**
1916
+ * Render an LLM-authored squisq dashboard document to a PNG via
1917
+ * squisq-cli's canonical dashboard renderer, on the service's managed
1918
+ * Chromium. The doc is pure markdown from the meester — no media — so
1919
+ * the content container is an empty read-only stub.
1920
+ */
1921
+ interface RenderAmbientDashboardOptions {
1922
+ home: string;
1923
+ markdown: string;
1924
+ outputPath: string;
1925
+ resolution: AmbientDashboardResolution;
1926
+ /** User-selected Squisq theme; overrides any model-authored frontmatter. */
1927
+ themeId: AmbientDashboardTheme;
1928
+ /** Full primary-display canvas plus its OS-chrome-safe content rectangle. */
1929
+ displayTarget?: AmbientDashboardDisplayTarget;
1930
+ style: AmbientDashboardStyle;
1931
+ /** Title-band fallback when the doc's frontmatter has none. */
1932
+ documentTitle: string;
1933
+ signal?: AbortSignal;
1934
+ }
1935
+ interface RenderAmbientDashboardResult {
1936
+ outputPath: string;
1937
+ width: number;
1938
+ height: number;
1939
+ /** Top-level block count of the parsed doc — the cell count. */
1940
+ blocks: number;
1941
+ }
1942
+ type AmbientDashboardRenderer = (opts: RenderAmbientDashboardOptions) => Promise<RenderAmbientDashboardResult>;
1943
+
1944
+ type DashboardOneShot = (prompt: string, timeoutMs: number, opts: {
1945
+ gezelId: string;
1946
+ jobLabel: string;
1947
+ tuningProfileId: 'instruct';
1948
+ lane: 'interactive' | 'background';
1949
+ ambient: boolean;
1950
+ }) => Promise<string>;
1951
+ interface AmbientDashboardGeneratorOptions {
1952
+ home: string;
1953
+ store: Store;
1954
+ history: HistoryManager;
1955
+ activity: ActivityTracker;
1956
+ oneShot: DashboardOneShot;
1957
+ renderer?: AmbientDashboardRenderer;
1958
+ /** Night shift counts as "scheduled work allowed". */
1959
+ isNightShiftActive?: () => boolean;
1960
+ isChatActive?: () => boolean;
1961
+ events?: Pick<ChatEventBus, 'publishGlobalEvent'>;
1962
+ intervalMs?: number;
1963
+ startupDelayMs?: number;
1964
+ now?: () => Date;
1965
+ }
1966
+ declare class AmbientDashboardGenerator {
1967
+ private readonly home;
1968
+ private readonly store;
1969
+ private readonly history;
1970
+ private readonly activity;
1971
+ private readonly oneShot;
1972
+ private readonly renderer;
1973
+ private readonly isNightShiftActive;
1974
+ private readonly isChatActive;
1975
+ private readonly events?;
1976
+ private readonly intervalMs;
1977
+ private readonly startupDelayMs;
1978
+ private readonly now;
1979
+ private startupTimer;
1980
+ private sweepTimer;
1981
+ private running;
1982
+ constructor(opts: AmbientDashboardGeneratorOptions);
1983
+ start(): void;
1984
+ stop(): void;
1985
+ isRunning(): boolean;
1986
+ readState(): Promise<AmbientDashboardState>;
1987
+ /** One automatic pass through the full gating chain. Exposed for tests. */
1988
+ sweep(): Promise<boolean>;
1989
+ /**
1990
+ * User-requested run. Bypasses the throttle and change gates — only
1991
+ * the enabled/meester/engagement floor applies.
1992
+ */
1993
+ runNow(): Promise<boolean>;
1994
+ private generate;
1995
+ /**
1996
+ * A failed run still advances `lastRunAt` — a chronically-failing
1997
+ * model must not retry every sweep. The input hash only advances on
1998
+ * success, so the next allowed run retries the same input.
1999
+ */
2000
+ private writeState;
2001
+ private collectCandidates;
2002
+ /**
2003
+ * Keep the newest `keep` dated PNGs. Only `dashboard-*.png` files are
2004
+ * eligible — never `latest.png`, the applier's `applied-*.png` slots,
2005
+ * or the state files. Stray temp files from a crashed render go too.
2006
+ */
2007
+ private prune;
2008
+ }
2009
+
2010
+ /**
2011
+ * Process-wide debug flag.
2012
+ *
2013
+ * Subsystems hold a shared reference and read `isEnabled()` at the
2014
+ * point of logging so a config flip — via `PUT /api/config` —
2015
+ * propagates instantly without re-spawning anything. The flag never
2016
+ * touches disk itself; the source of truth is `GezelConfig.debugMode`
2017
+ * in `~/.gezel/config.json`, re-read on service boot and on every
2018
+ * config PUT.
2019
+ */
2020
+ declare class DebugFlag {
2021
+ private _enabled;
2022
+ constructor(initial?: boolean);
2023
+ isEnabled(): boolean;
2024
+ set(next: boolean): void;
2025
+ }
2026
+
1698
2027
  /**
1699
2028
  * SecretStore — the one place secrets (API tokens, webhook keys, OAuth
1700
2029
  * access tokens) live at rest. Two implementations ship:
@@ -1766,115 +2095,22 @@ declare class ChannelManager {
1766
2095
  private readonly history;
1767
2096
  private readonly debug;
1768
2097
  private readonly channels;
1769
- private started;
1770
- constructor(opts: ChannelManagerOpts);
1771
- start(): Promise<void>;
1772
- stop(): Promise<void>;
1773
- /**
1774
- * Called by the config PUT handler when `channels.*` changed. Tears
1775
- * down every channel and rebuilds from the new config — cheap while
1776
- * only webhook (stateless) lives here.
1777
- */
1778
- reconfigure(): Promise<void>;
1779
- list(): Promise<ChannelStatus[]>;
1780
- send(message: string, opts?: SendOptions): Promise<SendResult & {
1781
- channel?: ChannelName;
1782
- }>;
1783
- private defaultChannel;
1784
- private applyConfig;
1785
- }
1786
-
1787
- type SessionListener = (event: ChatEvent) => void;
1788
- type EnvelopeListener = (envelope: ChatEventEnvelope) => void;
1789
- interface PublishScope {
1790
- sessionId: string;
1791
- gezelId: string;
1792
- projectId: string;
1793
- }
1794
- /**
1795
- * Per-session chat event bus, plus project-scoped and global fan-out for the
1796
- * interleaved timeline UI. Each `publish` reaches three subscriber sets:
1797
- *
1798
- * 1. Listeners on this session — bare `ChatEvent`. Get the recent history
1799
- * replayed when they subscribe (so a late session-scoped subscriber can
1800
- * catch up on the in-flight message). Cleared on `done`.
1801
- * 2. Listeners on this project — `ChatEventEnvelope` with sessionId/
1802
- * gezelId/projectId. Also get the history replayed for any in-flight
1803
- * sessions in this project, so a user who tabs away mid-stream and back
1804
- * doesn't lose the partial text + tools accumulated during the gap.
1805
- * 3. Global listeners — `ChatEventEnvelope`. Same replay semantics across
1806
- * every in-flight session anywhere.
1807
- *
1808
- * History is bus-side only: cleared on `done` so completed turns don't get
1809
- * re-delivered when a new subscriber connects after the fact.
1810
- *
1811
- * The early `fail()` path in `ChatManager.send` (when `ensureState` itself
1812
- * fails before we have a record) doesn't know the gezelId/projectId, so it
1813
- * publishes session-scoped only via `publishSessionOnly`. Project + global
1814
- * subscribers don't see those failures — only the session's SSE listener
1815
- * does, which is the only listener that cares about that scope.
1816
- */
1817
- declare class ChatEventBus {
1818
- private readonly sessionBuses;
1819
- private readonly projectListeners;
1820
- private readonly gezelListeners;
1821
- private readonly globalListeners;
1822
- publish(scope: PublishScope, event: ChatEvent): void;
1823
- /**
1824
- * Publish to the session bus only. Used when the manager's early-fail path
1825
- * doesn't know the gezel/project to envelope (the session record itself
1826
- * couldn't be loaded). Project + global subscribers never see these.
1827
- */
1828
- publishSessionOnly(sessionId: string, event: ChatEvent): void;
1829
- /**
1830
- * Publish a project-scoped lifecycle event that has no originating
1831
- * turn — e.g. `project_created`. Fans out to the project's listeners
1832
- * and all global listeners, but is deliberately NOT recorded in any
1833
- * session history: these are one-shot signals, not part of a
1834
- * replayable transcript, so a subscriber that reconnects later must
1835
- * not see them re-fired (unlike `publish`, which seeds `deliverToSession`
1836
- * history that replays until the turn's `done`). `sessionId`/`gezelId`
1837
- * are empty — the consumers that care (the sidebar refresh bridge)
1838
- * key off the event payload, not the envelope scope.
1839
- */
1840
- publishProjectEvent(projectId: string, event: ChatEvent): void;
1841
- /**
1842
- * Publish a project-less global lifecycle event — e.g. `night_shift`.
1843
- * Reaches only the global listeners (the Meester timeline SSE the UI
1844
- * subscribes to via `subscribeAll`); like `publishProjectEvent` it is
1845
- * not recorded in any session history, so reconnecting subscribers do
1846
- * not see it re-fired. The envelope scope fields are all empty;
1847
- * consumers key off the event payload.
1848
- */
1849
- publishGlobalEvent(event: ChatEvent): void;
1850
- subscribe(sessionId: string, listener: SessionListener): () => void;
1851
- subscribeProject(projectId: string, listener: EnvelopeListener): () => void;
1852
- subscribeGezel(gezelId: string, listener: EnvelopeListener): () => void;
1853
- subscribeAll(listener: EnvelopeListener): () => void;
1854
- private deliverToSession;
1855
- /**
1856
- * Append an event to a session's replay history, keeping the buffer
1857
- * lossless for the content that matters. Two rules:
1858
- *
1859
- * 1. Transient status pulses (`heartbeat`, `wire_pulse`) are delivered
1860
- * live but never recorded — they carry no replayable content, and a
1861
- * reasoning-heavy local-model turn emits enough of them to evict
1862
- * real deltas out of the 500-event cap.
1863
- * 2. Adjacent `delta` events coalesce into one entry. A five-minute
1864
- * thinking turn streams thousands of per-token deltas; stored
1865
- * individually they blow past the cap and the earliest thinking is
1866
- * silently dropped on replay. Merged, a text run costs one slot no
1867
- * matter how long it streamed. The stored entry is a fresh object —
1868
- * the original event was already handed to live listeners and must
1869
- * not be mutated.
2098
+ private started;
2099
+ constructor(opts: ChannelManagerOpts);
2100
+ start(): Promise<void>;
2101
+ stop(): Promise<void>;
2102
+ /**
2103
+ * Called by the config PUT handler when `channels.*` changed. Tears
2104
+ * down every channel and rebuilds from the new config — cheap while
2105
+ * only webhook (stateless) lives here.
1870
2106
  */
1871
- private recordInHistory;
1872
- private ensureSession;
1873
- private cleanupSession;
1874
- private pruneIdleSessions;
1875
- private safeDeliver;
1876
- /** Lightweight observability hook used by tests and diagnostics. */
1877
- activeSessionCount(): number;
2107
+ reconfigure(): Promise<void>;
2108
+ list(): Promise<ChannelStatus[]>;
2109
+ send(message: string, opts?: SendOptions): Promise<SendResult & {
2110
+ channel?: ChannelName;
2111
+ }>;
2112
+ private defaultChannel;
2113
+ private applyConfig;
1878
2114
  }
1879
2115
 
1880
2116
  /**
@@ -1987,6 +2223,18 @@ type HookRunner = (hook: ActiveHook, ctx: {
1987
2223
  type McpServerSpec = StdioMcpServerSpec | HttpMcpServerSpec;
1988
2224
  interface StdioMcpServerSpec {
1989
2225
  kind?: 'stdio';
2226
+ /**
2227
+ * Catalog toolset id this bridge speaks to, when it was spawned for an
2228
+ * installed toolset. Wrappers select on this rather than sniffing the
2229
+ * command line, because a managed system install spawns
2230
+ * `node <home>/system-toolsets/@playwright__mcp@0.0.78/package/cli.js` —
2231
+ * `installDirName` has slugified the `/` away, so the package name never
2232
+ * appears in `command`/`args`. Sniffing there left every Playwright
2233
+ * wrapper inert against the exact copy Gezel manages: the `file:`-URL
2234
+ * rewrite never fired, and the local-preview posture advertised a browser
2235
+ * surface nothing had pruned.
2236
+ */
2237
+ toolsetId?: string;
1990
2238
  command: string;
1991
2239
  args: string[];
1992
2240
  env: Record<string, string>;
@@ -1995,6 +2243,8 @@ interface StdioMcpServerSpec {
1995
2243
  }
1996
2244
  interface HttpMcpServerSpec {
1997
2245
  kind: 'http';
2246
+ /** Catalog toolset id — see {@link StdioMcpServerSpec.toolsetId}. */
2247
+ toolsetId?: string;
1998
2248
  /**
1999
2249
  * `streamable-http` is the current MCP HTTP transport (one URL,
2000
2250
  * POST for messages, optional GET for SSE stream + auto-reconnect).
@@ -2283,6 +2533,20 @@ interface PromptCtx extends ModelCtx {
2283
2533
  isMeester: boolean;
2284
2534
  /** The gezel's about.md content (already loaded). */
2285
2535
  about: string;
2536
+ /**
2537
+ * Tool names actually wired for this session (post-allowlist).
2538
+ *
2539
+ * A behavior whose whole point is "reach for tool X instead of tool Y"
2540
+ * must gate on this and return null when X is absent. The rendered block
2541
+ * is also passed through `filterPromptToolDirectives`, but that pass is
2542
+ * lexical: it drops a line only when the line reads as a directive, and
2543
+ * "write a small Node script and execute it — `derive_file(...)`" does
2544
+ * not match, so the block survived intact on a roster with no execution
2545
+ * tool at all and prescribed a remedy the session could not perform.
2546
+ * Positive gating here is the reliable half; the lexical filter is the
2547
+ * backstop for phrasings a behavior author did not anticipate.
2548
+ */
2549
+ availableToolNames: ReadonlySet<string>;
2286
2550
  }
2287
2551
  /**
2288
2552
  * Context for per-turn hooks: {@link Behavior.userPromptPrelude} and
@@ -2312,6 +2576,17 @@ interface TurnCtx extends ModelCtx {
2312
2576
  assistantContent: string;
2313
2577
  /** How many continuation nudges have already fired in this user-initiated send. */
2314
2578
  continuationCount: number;
2579
+ /**
2580
+ * Shared-library documents matching THIS turn's message, resolved before
2581
+ * the prelude hooks run (they are synchronous; the search is not).
2582
+ * Populated only for user-origin turns, and only with matches strong
2583
+ * enough to be worth the tokens — usually absent.
2584
+ */
2585
+ libraryRecall?: ReadonlyArray<{
2586
+ path: string;
2587
+ snippet: string;
2588
+ score: number;
2589
+ }>;
2315
2590
  }
2316
2591
  /**
2317
2592
  * Verdict returned by {@link Behavior.postTurnDetector} when a
@@ -2656,6 +2931,18 @@ interface QuotaBucket {
2656
2931
  overage: number;
2657
2932
  resetDate?: string;
2658
2933
  }
2934
+ interface UsageTurn {
2935
+ model: string;
2936
+ inputTokens: number;
2937
+ /** Portion of `inputTokens` served from the provider's prompt cache; absent when unreported. */
2938
+ cachedInputTokens?: number;
2939
+ outputTokens: number;
2940
+ cost: number;
2941
+ durationMs: number;
2942
+ /** Engine-reported decode rate for this turn; absent when unreported. */
2943
+ outputTokensPerSec?: number;
2944
+ at: string;
2945
+ }
2659
2946
  interface ProviderUsage {
2660
2947
  /** Non-empty only for providers that report account quota windows. */
2661
2948
  quotaBuckets: QuotaBucket[];
@@ -2686,8 +2973,30 @@ interface ProviderUsage {
2686
2973
  * "n/a" rather than zero.
2687
2974
  */
2688
2975
  medianOutputTokensPerSec: number | null;
2976
+ /**
2977
+ * The same median decode rate split per model, for the turns that reported
2978
+ * one. A 27B and a 4B averaged together is a number describing nothing, and
2979
+ * the question a user actually asks — "how fast is this model on my machine"
2980
+ * — is per model. Sorted fastest first; empty for providers that report no
2981
+ * throughput at all.
2982
+ *
2983
+ * This is the durable half of the speed story: it spans every session the
2984
+ * daemon has served, so it is still there when a page reloads mid-turn and
2985
+ * the client-side rolling window is empty.
2986
+ */
2987
+ modelSpeeds: ModelSpeed[];
2988
+ /** Most recently completed turn, retained independently of the UI's rolling window. */
2989
+ lastTurn: UsageTurn | null;
2689
2990
  lastUpdated: string | null;
2690
2991
  }
2992
+ /** Per-model decode-rate rollup inside {@link ProviderUsage}. */
2993
+ interface ModelSpeed {
2994
+ model: string;
2995
+ /** Median engine-reported decode rate across this model's turns. */
2996
+ medianOutputTokensPerSec: number;
2997
+ /** Turns that contributed a rate — not the model's total turn count. */
2998
+ turns: number;
2999
+ }
2691
3000
  interface UsageSummary {
2692
3001
  providers: {
2693
3002
  copilot?: ProviderUsage;
@@ -2716,6 +3025,8 @@ declare class UsageTracker {
2716
3025
  * those windows without inventing a zero-token turn or skewing totals.
2717
3026
  */
2718
3027
  recordQuotaBuckets(provider: ProviderName, buckets: QuotaBucket[], at?: string): void;
3028
+ /** Latest quota buckets recorded for `provider`; empty when never reported. */
3029
+ quotaBucketsFor(provider: ProviderName): QuotaBucket[];
2719
3030
  summary(): UsageSummary;
2720
3031
  private ensure;
2721
3032
  }
@@ -2768,7 +3079,7 @@ type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number];
2768
3079
  * — no wasted work. Already-running entries abort normally via the
2769
3080
  * same signal.
2770
3081
  *
2771
- * ## Ambient admission control (single-lane engine protection)
3082
+ * ## Ambient admission control (foreground-capacity protection)
2772
3083
  *
2773
3084
  * On a local engine, a housekeeping turn (scheduler nudge, memory
2774
3085
  * extraction, icon/about generation) occupies the GPU for minutes.
@@ -2778,17 +3089,19 @@ type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number];
2778
3089
  * engine is idle while the user READS, ambient work sneaks in, and
2779
3090
  * the user's move then waits out a 3-minute nudge turn).
2780
3091
  *
2781
- * When `ambientQuietMs > 0`, entries marked `ambient: true` dispatch
2782
- * only when BOTH hold: no non-ambient work is running or pending, and
2783
- * no non-ambient activity (enqueue, dispatch, or release, any lane)
2784
- * happened within the last `ambientQuietMs`. Reading time counts as
2785
- * engagement: page reactions and tool-loop turns are non-ambient, so
2786
- * an active game holds the gate closed between moves. Ambient entries
2787
- * are exempt from the anti-starvation guard while gated deferring
2788
- * them indefinitely during active use is the feature, not a bug; they
2789
- * run when the user steps away. The gate never touches non-ambient
2790
- * work, so mid-turn compaction (which a foreground turn awaits) and
2791
- * user-facing background turns are unaffected.
3092
+ * When an engine has more than one interactive slot, ambient work may
3093
+ * use otherwise-idle capacity immediately, but only while at least one
3094
+ * physical slot stays free for a newly-arriving foreground chat. On a
3095
+ * single-slot engine or once a multi-slot engine reaches that protected
3096
+ * headroom entries marked `ambient: true` dispatch only when BOTH hold:
3097
+ * no non-ambient work is running or pending, and no non-ambient activity
3098
+ * (enqueue, dispatch, or release, any lane) happened within the last
3099
+ * `ambientQuietMs`. Reading time counts as engagement: page reactions and
3100
+ * tool-loop turns are non-ambient, so an active game holds the gate closed
3101
+ * between moves when there is no safe spare capacity. Ambient entries are
3102
+ * exempt from the anti-starvation guard while gated. The gate never touches
3103
+ * non-ambient work, so mid-turn compaction (which a foreground turn awaits)
3104
+ * and user-facing background turns are unaffected.
2792
3105
  */
2793
3106
  type Lane = 'interactive' | 'background';
2794
3107
  interface EnqueueRequest {
@@ -2797,9 +3110,10 @@ interface EnqueueRequest {
2797
3110
  * Marks truly-deferrable housekeeping (scheduler nudges, memory
2798
3111
  * extraction, icon/about generation, digests, night-shift chores).
2799
3112
  * When the queue was built with {@link ProviderQueueOptions.ambientQuietMs},
2800
- * ambient entries dispatch only after a quiet window with no
2801
- * NON-ambient activity — see "Ambient admission control" in the
2802
- * class header. Distinct from `lane: 'background'`: background
3113
+ * ambient entries use safe spare capacity immediately, or otherwise
3114
+ * wait for a quiet window with no NON-ambient activity — see "Ambient
3115
+ * admission control" in the class header. Distinct from `lane:
3116
+ * 'background'`: background
2803
3117
  * covers anything that shouldn't cut in front of a typed chat turn,
2804
3118
  * which includes user-facing work (checkers page reactions, mid-turn
2805
3119
  * compaction a foreground turn awaits) that must NEVER be held.
@@ -2906,10 +3220,12 @@ interface ProviderQueueOptions {
2906
3220
  maxWaitMs?: number;
2907
3221
  /**
2908
3222
  * Quiet window for ambient admission control — see the class header.
2909
- * Entries marked `ambient: true` dispatch only after this many ms
2910
- * with no non-ambient activity (and none running or pending). 0 or
2911
- * unset disables the gate (ambient entries behave like plain
2912
- * background). Local single-GPU engines pass
3223
+ * On a multi-slot engine, entries marked `ambient: true` may dispatch
3224
+ * sooner while one physical foreground slot remains protected. Once
3225
+ * that headroom is reached, they wait for this many ms with no
3226
+ * non-ambient activity (and none running or pending). 0 or unset
3227
+ * disables the gate (ambient entries behave like plain background).
3228
+ * Local single-GPU engines pass
2913
3229
  * {@link defaultAmbientQuietMs}; cloud queues leave it off.
2914
3230
  */
2915
3231
  ambientQuietMs?: number;
@@ -2975,6 +3291,14 @@ declare class ProviderQueue {
2975
3291
  */
2976
3292
  describe(): {
2977
3293
  running: number;
3294
+ /**
3295
+ * In-flight slots split by lane. `running` alone cannot answer "how
3296
+ * many conversations are live right now?" — a queue holding nothing
3297
+ * but index-enrichment one-shots still reports `running: 1`, which
3298
+ * the pill used to render as "1 running" beside a chat-shaped noun.
3299
+ */
3300
+ runningInteractive: number;
3301
+ runningBackground: number;
2978
3302
  queuedInteractive: number;
2979
3303
  queuedBackground: number;
2980
3304
  /** Pending ambient entries currently held by the admission gate. */
@@ -3038,9 +3362,15 @@ declare class ProviderQueue {
3038
3362
  */
3039
3363
  private maybeScheduleAmbientRecheck;
3040
3364
  /**
3041
- * True when ambient entries may dispatch: gate disabled, or no
3042
- * non-ambient work anywhere on the queue AND the quiet window has
3043
- * fully elapsed since the last non-ambient activity.
3365
+ * True when ambient entries may dispatch.
3366
+ *
3367
+ * A multi-slot engine may spend spare capacity immediately while keeping
3368
+ * one of its physical interactive slots unused. `running` is deliberately
3369
+ * compared with `interactiveConcurrency - 1`, rather than with the queue's
3370
+ * total `concurrency`: local queues carry one extra logical background lane
3371
+ * for deadlock-breaking one-shots, but that lane is not another native KV
3372
+ * slot. Once protected headroom is exhausted, the ordinary quiet-window
3373
+ * rule applies.
3044
3374
  */
3045
3375
  private ambientGateOpen;
3046
3376
  /**
@@ -3818,9 +4148,10 @@ interface LLMSession {
3818
4148
  onDelta(handler: (chunk: string) => void): () => void;
3819
4149
  /**
3820
4150
  * Subscribe to live private-reasoning deltas, streamed separately from
3821
- * the visible reply so they never enter the committed body or the
3822
- * external API-compat forwarders. Optional only providers with a
3823
- * distinct reasoning channel (llama-cpp/ds4) fire it.
4151
+ * the visible reply so they never enter the committed body. API-compat
4152
+ * forwarders omit this private channel unless their client contract opts
4153
+ * in explicitly. Optional only providers with a distinct reasoning
4154
+ * channel (llama-cpp/ds4) fire it.
3824
4155
  */
3825
4156
  onReasoningDelta?(handler: (chunk: string) => void): () => void;
3826
4157
  onUsage(handler: (usage: TurnUsage) => void): () => void;
@@ -3845,7 +4176,7 @@ interface LLMSession {
3845
4176
  * calls arrive whole (Ollama) or run server-side (Copilot, OpenAI)
3846
4177
  * leave it undefined.
3847
4178
  */
3848
- onToolArgsDelta?(handler: (name: string, chunk: string) => void): () => void;
4179
+ onToolArgsDelta?(handler: (name: string, chunk: string, meta?: ToolArgsDeltaMeta) => void): () => void;
3849
4180
  /**
3850
4181
  * Optional: subscribe to phase-announcement events. Currently only
3851
4182
  * `CopilotSession` emits these (SDK `assistant.intent` events from
@@ -3972,6 +4303,13 @@ interface ExternalToolCall {
3972
4303
  name: string;
3973
4304
  arguments: string;
3974
4305
  }
4306
+ /** Identity attached to a live structured tool-argument fragment. */
4307
+ interface ToolArgsDeltaMeta {
4308
+ /** OpenAI-compatible position within the assistant's tool_calls array. */
4309
+ index?: number;
4310
+ /** Provider-supplied call id, normally present on the first fragment. */
4311
+ id?: string;
4312
+ }
3975
4313
  /**
3976
4314
  * Context describing one would-be batch member, passed to
3977
4315
  * {@link BatchCapability.admit} so an engine can make a live headroom
@@ -4400,11 +4738,14 @@ declare class CapacityBroker {
4400
4738
  /**
4401
4739
  * Fallback estimate when a catalog entry lacks `residentBytes`.
4402
4740
  * Used by callers building reservation requests from
4403
- * `approxSizeBytes`. Multipliers reflect typical working-set
4404
- * footprint vs. on-disk size at default context.
4741
+ * `approxSizeBytes`. Reflects the working-set footprint vs. on-disk
4742
+ * size WITHOUT any KV every caller prices KV explicitly on top.
4405
4743
  *
4406
- * llama.cpp at Q4_K_M with 8K ctx: ~1.20 × on-disk.
4407
- * MLX at 4bit with 8K ctx: ~1.30 × on-disk (KV grows faster).
4744
+ * llama.cpp: see {@link estimateLlamaCppResidentBytes}. Pass `mmprojBytes`
4745
+ * for a multimodal model `approxSizeBytes` counts the weights alone, and
4746
+ * the projector plus its vision buffers is 1.7 GB on a 30B.
4747
+ * MLX: see {@link estimateMlxResidentBytes}. MLX needs no equivalent — its
4748
+ * vision tower is inside the same safetensors set `approxSizeBytes` sums.
4408
4749
  *
4409
4750
  * ds4 is special: it streams MoE experts from SSD, so its resident
4410
4751
  * working set is bounded by the configured expert-cache budget + KV +
@@ -4413,7 +4754,9 @@ declare class CapacityBroker {
4413
4754
  * conservative streaming working set so a 64GB box is never told an
4414
4755
  * 87GB DeepSeek-V4 model "can't fit" (which is the whole point of ds4).
4415
4756
  */
4416
- static estimateResidentBytes(engine: LocalProviderName, approxSizeBytes: number): number;
4757
+ static estimateResidentBytes(engine: LocalProviderName, approxSizeBytes: number, opts?: {
4758
+ mmprojBytes?: number;
4759
+ }): number;
4417
4760
  }
4418
4761
 
4419
4762
  /**
@@ -4582,6 +4925,9 @@ interface PoolSnapshot {
4582
4925
  */
4583
4926
  interface PooledQueueSummary {
4584
4927
  running: number;
4928
+ /** See {@link ProviderQueue.describe}. Summed across replicas like `running`. */
4929
+ runningInteractive: number;
4930
+ runningBackground: number;
4585
4931
  queuedInteractive: number;
4586
4932
  queuedBackground: number;
4587
4933
  /** Pending ambient entries held by the admission gate across replicas. */
@@ -4664,6 +5010,18 @@ declare class ProviderPool {
4664
5010
  * Returns false when the engine has already gone away.
4665
5011
  */
4666
5012
  unloadIdle(key: string): Promise<boolean>;
5013
+ /**
5014
+ * Unload every resident engine that is idle right now, leaving busy ones
5015
+ * running. Returns the keys that stayed resident because they were serving
5016
+ * a turn — the caller reports them; the ordinary LRU / idle-retention path
5017
+ * collects them once they go quiet.
5018
+ *
5019
+ * This is what a live config reset uses. {@link shutdown} is the other
5020
+ * shape — it force-evicts busy engines too — and is only correct when the
5021
+ * process is going away (service shutdown, emergency stop), because a
5022
+ * forced eviction kills whatever turn is streaming.
5023
+ */
5024
+ releaseIdle(): Promise<string[]>;
4667
5025
  /**
4668
5026
  * Evict a specific entry. Awaits the provider's `shutdown` so the
4669
5027
  * disk-cache flush (Tier 1 prior plan) completes before the broker
@@ -5192,6 +5550,17 @@ declare class ContentIndex {
5192
5550
  stale: number;
5193
5551
  pending: number;
5194
5552
  } | null>;
5553
+ /**
5554
+ * Indexing work this project recorded in `[since, until)` — summaries,
5555
+ * reviews, and AI shadows, from the tiers' own stamps. Null when the
5556
+ * project has no index (or it's momentarily unavailable); callers tally
5557
+ * across projects and treat that as "nothing to add", never as zero work.
5558
+ */
5559
+ workCountsSince(projectId: string, since: string, until: string): Promise<{
5560
+ summarized: number;
5561
+ reviewed: number;
5562
+ described: number;
5563
+ } | null>;
5195
5564
  /** One file's boekwachter review (`file_review` tool). */
5196
5565
  fileReview(projectId: string, relPath: string): Promise<FileReviewResponse>;
5197
5566
  /** Flat issue query across current-hash reviews (`list_file_issues` tool). */
@@ -5206,6 +5575,28 @@ declare class ContentIndex {
5206
5575
  settleBoekwachterIssuesForTask(projectId: string, taskRef: string, outcome: 'complete' | 'canceled'): Promise<number>;
5207
5576
  private boekwachterIssuesForPath;
5208
5577
  searchDocs(projectId: string, query: string, maxResults?: number): Promise<SearchDocsResponse>;
5578
+ /**
5579
+ * Content search over the shared document library.
5580
+ *
5581
+ * The library is a project, so this is the ordinary hybrid searcher
5582
+ * (embeddings + keyword) reshaped to document identity: callers name a
5583
+ * document by the path the user filed it under, never by the converted
5584
+ * markdown the snippet happened to come from. That mapping is why this
5585
+ * exists rather than callers using `searchCode` directly.
5586
+ */
5587
+ searchLibrary(projectId: string, query: string, opts?: {
5588
+ maxResults?: number;
5589
+ queryVector?: number[];
5590
+ }): Promise<SearchDocumentsResponse>;
5591
+ /**
5592
+ * One-line descriptions for library documents, keyed by path.
5593
+ *
5594
+ * Two sources, in precedence order: the document's own frontmatter
5595
+ * `description`/`title` — the user said what this is, so nothing should
5596
+ * outrank it — then the Boekwachter's generated summary. Absent entries
5597
+ * simply have no description; a listing renders the bare path.
5598
+ */
5599
+ libraryDescriptions(projectId: string): Promise<Map<string, string>>;
5209
5600
  readDocAsMarkdown(projectId: string, relPath: string): Promise<ReadDocAsMarkdownResponse>;
5210
5601
  /**
5211
5602
  * Schedule a rebuild of the project's artifacts-corpus index. Debounced per
@@ -5788,6 +6179,19 @@ interface GateCheckOutcome {
5788
6179
  detail: string;
5789
6180
  /** Machine evidence preserved from the rich CheckResults (arrays sliced to ≤10). */
5790
6181
  evidence?: Record<string, unknown>;
6182
+ /**
6183
+ * Discrete items this check still wants, for checks that count in items
6184
+ * at all (unread corpus records, uncovered paths). Folded into the
6185
+ * plateau signature, which is what lets a gate a craftbook is DESIGNED
6186
+ * to fail repeatedly — a bounded batch loop — read as progress instead
6187
+ * of a stall while the count falls.
6188
+ *
6189
+ * Only set this where the count is deterministic for a given
6190
+ * deliverable: a number that jitters on identical content would reset
6191
+ * the ladder forever and hide a real plateau. Leaving it unset keeps
6192
+ * the legacy identity-only behavior exactly.
6193
+ */
6194
+ remaining?: number;
5791
6195
  }
5792
6196
  interface GateCheckResult {
5793
6197
  pass: boolean;
@@ -5875,11 +6279,17 @@ declare function gateCheckLabel(c: GateCheck): string;
5875
6279
  * stage 3 — stop: pause with the full attempt trail as diagnosis.
5876
6280
  *
5877
6281
  * The plateau signature hashes failing-check IDENTITY (GateCheckOutcome
5878
- * labels), not prose and not content bytes: byte churn with an unmoved
5879
- * failure set IS the plateau (the eval harness's retryLoopSniffKey
5880
- * lesson), while clearing any single check changes the signature and
5881
- * resets the ladder. Kill-switch: GEZEL_DISABLE_GATE_ESCALATION=1
5882
- * restores the legacy quiet-damper behavior.
6282
+ * labels) plus any outstanding-item count those checks report, not prose
6283
+ * and not content bytes: byte churn with an unmoved failure set IS the
6284
+ * plateau (the eval harness's retryLoopSniffKey lesson), while clearing a
6285
+ * check or covering more of what a counting check still wants — changes
6286
+ * the signature and resets the ladder. The count matters because some
6287
+ * craftbooks are DESIGNED to fail a gate repeatedly (bounded batch loops
6288
+ * over a corpus too large for one context); identity alone cannot tell
6289
+ * those from a stall, and calling them a stall pushes the assignee toward
6290
+ * the smallest edit that clears the check rather than the work.
6291
+ * Kill-switch: GEZEL_DISABLE_GATE_ESCALATION=1 restores the legacy
6292
+ * quiet-damper behavior.
5883
6293
  */
5884
6294
 
5885
6295
  type EscalationStage = 0 | 1 | 2 | 3;
@@ -6437,14 +6847,18 @@ declare class TaskManager {
6437
6847
  */
6438
6848
  private runCompletionGate;
6439
6849
  /**
6440
- * A rejected gate is unsatisfiable when a FAILING check reads the
6441
- * workspace tree while gezel workspace writes are off for the project
6442
- * (gate scripts always read the workspace, so a script rejection
6443
- * qualifies too). Returns the workspace files the failing checks name
6444
- * (possibly empty), or null when the gate is repairable. Drawer-only
6445
- * failures stay repairable — the artifacts drawer is deliberately
6446
- * exempt from the writes-off policy.
6850
+ * True when EVERY one of `tasks` is parked on a step that owes a
6851
+ * workspace file managed writes can't produce. Lets the ambient nudger
6852
+ * tell a genuinely stuck writes-off project from one doing artifact-only
6853
+ * work the drawer is exempt from the writes-off policy, so a nightly
6854
+ * report or a review still completes there and its voorman still wants
6855
+ * the check-in.
6856
+ *
6857
+ * Conservative by construction: an empty list, a task with no active
6858
+ * step, or any step whose deliverable is satisfiable all answer `false`.
6859
+ * We only claim "blocked" on positive evidence for every task.
6447
6860
  */
6861
+ allActiveStepsBlockedByWorkspaceWrites(projectId: string, tasks: Task[]): Promise<boolean>;
6448
6862
  /**
6449
6863
  * The workspace files a step must produce that no assignee is able to
6450
6864
  * write. The proactive twin of {@link unsatisfiableWorkspaceGateFiles}:
@@ -6465,6 +6879,22 @@ declare class TaskManager {
6465
6879
  * must skip the handoff — dispatching a gezel here cannot succeed.
6466
6880
  */
6467
6881
  private pauseIfStepUnsatisfiable;
6882
+ /**
6883
+ * A rejected gate is unsatisfiable when a FAILING check reads the
6884
+ * workspace tree while gezel workspace writes are off for the project.
6885
+ * Returns the workspace files the failing checks name (possibly empty),
6886
+ * or null when the gate is repairable. Drawer-only failures stay
6887
+ * repairable — the artifacts drawer is deliberately exempt from the
6888
+ * writes-off policy.
6889
+ *
6890
+ * A gate SCRIPT names no file, so it is judged by what the step itself
6891
+ * owes: unwinnable only when the step's own deliverable is a workspace
6892
+ * file nobody can write. Counting every script rejection as a
6893
+ * workspace-write failure misread the note-only gate on Pull Request
6894
+ * Review's scope step — the note was written correctly and the real
6895
+ * fault was elsewhere, but the task paused telling the user to enable
6896
+ * workspace writes, which would have changed nothing.
6897
+ */
6468
6898
  private unsatisfiableWorkspaceGateFiles;
6469
6899
  /**
6470
6900
  * Gate-driven loop-back: re-activate `targetId` WITHOUT completing the
@@ -6472,6 +6902,11 @@ declare class TaskManager {
6472
6902
  * hook even when the target IS the gated step itself — `onReject:
6473
6903
  * <self>` deliberately produces a fresh activation + handoff session
6474
6904
  * each rejection (the loop shape that carries small models).
6905
+ *
6906
+ * A fresh SESSION is not a fresh BUDGET, though: when the route points
6907
+ * back at the gated step, the rejection count has to survive the
6908
+ * re-activation or `maxAttempts` is unreachable and the loop never
6909
+ * escalates. See the self-loop carve-out in {@link bumpStepActivation}.
6475
6910
  */
6476
6911
  private reactivateStepForGate;
6477
6912
  /**
@@ -6848,6 +7283,12 @@ declare class EngineRouter {
6848
7283
  * Provider is fixed to the caller's chosen engine.
6849
7284
  */
6850
7285
  reconcileClones(provider: LocalProviderName, target: Record<string, number>): Promise<void>;
7286
+ /**
7287
+ * Unload every resident replica that is not mid-turn. Returns the keys left
7288
+ * resident because they were busy. Used by a live config reset, which needs
7289
+ * the memory back but must not sever work in flight.
7290
+ */
7291
+ releaseIdle(): Promise<string[]>;
6851
7292
  /** Unload one resident replica immediately, provided it is still idle. */
6852
7293
  unloadIdle(provider: LocalProviderName, modelId: string, replicaIdx: number): Promise<boolean>;
6853
7294
  snapshot(): PoolSnapshot;
@@ -7610,6 +8051,35 @@ interface ModelBundleSource {
7610
8051
  catalogVersion?: string;
7611
8052
  }
7612
8053
 
8054
+ /**
8055
+ * "Is a newer build of this model available?" — asked of the payload, not of
8056
+ * the version string.
8057
+ *
8058
+ * Both engine managers (llama.cpp/ds4 and MLX) and the chat manager's
8059
+ * per-session staleness notice used to answer this by comparing the installed
8060
+ * `catalogVersion` to the catalog's current one. That flags every catalog
8061
+ * edit, including the metadata-only ones the runtime has already picked up,
8062
+ * and the only remedy any surface offers is a multi-gigabyte re-download. Here
8063
+ * the version difference is the *trigger* for a check, and
8064
+ * {@link comparePayloadIdentity} is the answer.
8065
+ *
8066
+ * When the payload turns out to be unchanged we record that finding in the
8067
+ * install manifest (a `payloadFingerprint`) so later checks are a string
8068
+ * compare. `catalogVersion` is deliberately left alone: it means "the version
8069
+ * whose description these bytes were downloaded against", which is what the
8070
+ * `.gezmodel` exporter pins and what model-fitness records compare, and
8071
+ * rewriting it would silently invalidate both.
8072
+ */
8073
+
8074
+ interface ModelUpdateStatus {
8075
+ /** True only when the catalog's payload differs from the copy on disk. */
8076
+ updateAvailable: boolean;
8077
+ /** The catalog's current version, when it differs from the installed one. */
8078
+ availableVersion?: string;
8079
+ /** Why, in one sentence — surfaced in the model manager's tooltip. */
8080
+ reason?: string;
8081
+ }
8082
+
7613
8083
  interface IncompleteModelDownload {
7614
8084
  id: string;
7615
8085
  /** Total bytes on disk across the directory (payload + `.partial`). */
@@ -7712,6 +8182,19 @@ interface InstalledMlxModel {
7712
8182
  * Delete that can only fail.
7713
8183
  */
7714
8184
  readOnly?: boolean;
8185
+ /**
8186
+ * True when the catalog now describes a different PAYLOAD than the one on
8187
+ * disk — different repo, file list, or hashes. A version bump that only
8188
+ * edits metadata (tuning, sizing hints, wording) deliberately does not set
8189
+ * this: the runtime resolves that live from the catalog, so there is nothing
8190
+ * for the user to download. Computed server-side in
8191
+ * {@link MlxModelManager.listInstalled} so every surface agrees.
8192
+ */
8193
+ updateAvailable?: boolean;
8194
+ /** The catalog's current version, when it differs from the installed one. */
8195
+ availableVersion?: string;
8196
+ /** What changed, in one sentence, for the model manager's tooltip. */
8197
+ updateReason?: string;
7715
8198
  }
7716
8199
  /**
7717
8200
  * Snapshot of an in-flight MLX install — same shape as
@@ -7831,6 +8314,19 @@ declare class MlxModelManager {
7831
8314
  constructor(opts: MlxModelManagerOptions);
7832
8315
  getActiveInstalls(): MlxActiveInstallSnapshot[];
7833
8316
  listInstalled(): Promise<InstalledMlxModel[]>;
8317
+ /**
8318
+ * Whether a genuinely newer build of one installed model exists. Null when
8319
+ * the id isn't installed for MLX. Shared with the chat manager's
8320
+ * per-session staleness notice so a user is never told to re-download for a
8321
+ * change that only touched metadata.
8322
+ */
8323
+ getUpdateStatus(id: string): Promise<ModelUpdateStatus | null>;
8324
+ /**
8325
+ * Compare one installed copy against the live catalog. The catalog lookup
8326
+ * runs for every model on every inventory poll; the manifest re-read only
8327
+ * happens for the few whose version actually moved.
8328
+ */
8329
+ private evaluateDrift;
7834
8330
  /**
7835
8331
  * List incomplete MLX downloads — directories that hold bytes but no
7836
8332
  * `manifest.json`, so they never surface in {@link listInstalled}. Tagged
@@ -7878,6 +8374,14 @@ declare class MlxModelManager {
7878
8374
  * worker owns the error.
7879
8375
  */
7880
8376
  private downloadFileConcurrent;
8377
+ /**
8378
+ * Files the plan asks for that are already on disk as exactly the bytes the
8379
+ * catalog pins, keyed by filename with the digest to carry forward. See
8380
+ * {@link LlamaCppModelManager.planReusableFiles} — same trust model: a
8381
+ * digest this install recorded after verifying it, plus a file still at its
8382
+ * pinned length, is identity.
8383
+ */
8384
+ private planReusableFiles;
7881
8385
  /** Warn once per model directory per process, then stay quiet. */
7882
8386
  private warnSkip;
7883
8387
  private loadInstalled;
@@ -8059,6 +8563,15 @@ interface InstalledLlamaCppModel {
8059
8563
  * video inputs on llama-server. Absent → text-only model.
8060
8564
  */
8061
8565
  mmprojPath?: string;
8566
+ /**
8567
+ * On-disk size of {@link mmprojPath}, read from the file rather than the
8568
+ * manifest. `approxSizeBytes` does NOT include the projector — the value
8569
+ * recorded at install time is the weights alone whenever the payload was
8570
+ * adopted or resumed rather than freshly downloaded — so every memory
8571
+ * estimate has to add this explicitly or it under-reserves a vision model
8572
+ * by the projector plus its compute buffers (1.7 GB on muse-glimmer).
8573
+ */
8574
+ mmprojSizeBytes?: number;
8062
8575
  /**
8063
8576
  * Absolute path to a speculative-decoding companion GGUF, when the
8064
8577
  * catalog ships one. The launcher forwards it as
@@ -8097,15 +8610,20 @@ interface InstalledLlamaCppModel {
8097
8610
  */
8098
8611
  readOnly?: boolean;
8099
8612
  /**
8100
- * True when the catalog now ships a different version than the one this
8101
- * model was downloaded against — a newer build is available. Lets the
8102
- * model manager offer "Update" (re-download + replace in place) instead
8103
- * of forcing the user to delete and re-fetch. Computed in
8613
+ * True when the catalog now describes a different PAYLOAD than the one on
8614
+ * disk — a newer build whose bytes this copy does not have. Lets the model
8615
+ * manager offer "Update" (fetch what differs, replace in place) instead of
8616
+ * forcing the user to delete and re-fetch. A catalog version bump that
8617
+ * leaves the files alone (retuning, sizing hints, wording) deliberately
8618
+ * does not set this: the runtime resolves that metadata live, so there is
8619
+ * nothing for the user to download. Computed in
8104
8620
  * {@link LlamaCppModelManager.listInstalled} against the live catalog.
8105
8621
  */
8106
8622
  updateAvailable?: boolean;
8107
8623
  /** The catalog's current version, when it differs from the installed one. */
8108
8624
  availableVersion?: string;
8625
+ /** What changed, in one sentence, for the model manager's tooltip. */
8626
+ updateReason?: string;
8109
8627
  }
8110
8628
  type InstallEvent = {
8111
8629
  type: 'progress';
@@ -8301,6 +8819,19 @@ declare class LlamaCppModelManager {
8301
8819
  * needs the `contextWindow` to set `--ctx-size` correctly.
8302
8820
  */
8303
8821
  resolveModel(id: string): Promise<InstalledLlamaCppModel | null>;
8822
+ /**
8823
+ * Whether a genuinely newer build of one installed model exists. Null when
8824
+ * the id isn't installed for this engine. Shared with the chat manager's
8825
+ * per-session staleness notice so a user is never told to re-download for a
8826
+ * change that only touched metadata.
8827
+ */
8828
+ getUpdateStatus(id: string): Promise<ModelUpdateStatus | null>;
8829
+ /**
8830
+ * Compare one installed copy against the live catalog. The catalog lookup
8831
+ * runs for every model on every inventory poll; the manifest re-read only
8832
+ * happens for the few whose version actually moved.
8833
+ */
8834
+ private evaluateDrift;
8304
8835
  delete(id: string): Promise<void>;
8305
8836
  /** Provider-owned snapshot used by the streaming `.gezmodel` exporter. */
8306
8837
  getModelBundleSource(id: string): Promise<ModelBundleSource>;
@@ -8336,6 +8867,19 @@ declare class LlamaCppModelManager {
8336
8867
  * cleanup with each early `return` and silently rot.
8337
8868
  */
8338
8869
  private runInstall;
8870
+ /**
8871
+ * Files the plan asks for that are already on disk as exactly the bytes the
8872
+ * catalog pins, keyed by filename with the digest to carry forward.
8873
+ *
8874
+ * An update usually rotates one thing — a quant, one shard, an added draft
8875
+ * companion — and re-fetching the rest is pure waste. The install manifest
8876
+ * records a sha256 for every file it published, each verified against the
8877
+ * catalog when it landed; where that digest equals the pin we are about to
8878
+ * download and the file is still its full length, the bytes on disk ARE the
8879
+ * bytes we would fetch. Same trust model as the shared-store adoption cache:
8880
+ * a recorded digest plus an unchanged stat is identity.
8881
+ */
8882
+ private planReusableFiles;
8339
8883
  /** Warn once per model directory per process, then stay quiet. */
8340
8884
  private warnSkip;
8341
8885
  private loadInstalled;
@@ -8437,6 +8981,55 @@ interface RemotesRegistry {
8437
8981
  touch(remoteId: string): void;
8438
8982
  }
8439
8983
 
8984
+ interface ExternalTranscriptToolCall {
8985
+ id: string;
8986
+ name: string;
8987
+ arguments: string;
8988
+ }
8989
+ interface ExternalTranscriptMessage {
8990
+ role: 'system' | 'developer' | 'user' | 'assistant' | 'tool';
8991
+ content: string;
8992
+ toolCalls?: ExternalTranscriptToolCall[];
8993
+ toolCallId?: string;
8994
+ }
8995
+ interface BeginExternalConversationInput {
8996
+ sourceId: string;
8997
+ sourceName: string;
8998
+ externalConversationId: string;
8999
+ workingDirectory?: string;
9000
+ projectHint?: string;
9001
+ gezelId: string;
9002
+ providerName: ProviderName;
9003
+ model?: string;
9004
+ messages: ExternalTranscriptMessage[];
9005
+ /** Effective prompt and tool roster that were actually supplied this request. */
9006
+ effectiveSystemMessage: string;
9007
+ toolNames: string[];
9008
+ /** Receipt block injected into the model-facing final tool result, when any. */
9009
+ actionLedger?: string;
9010
+ }
9011
+ interface ResolveExternalConversationInput {
9012
+ sourceId: string;
9013
+ gezelId: string;
9014
+ messages: ExternalTranscriptMessage[];
9015
+ /** Unique id for a genuinely new thread, normally the caller's request id. */
9016
+ fallbackExternalConversationId: string;
9017
+ }
9018
+ interface ExternalConversationFinish {
9019
+ content: string;
9020
+ reasoning?: string;
9021
+ finishReason: 'stop' | 'tool_calls' | 'length';
9022
+ }
9023
+ interface ExternalConversationTurn {
9024
+ sessionId: string;
9025
+ projectId: string;
9026
+ onContentDelta(content: string): void;
9027
+ onReasoningDelta(content: string): void;
9028
+ onToolArgsDelta(name: string, content: string): void;
9029
+ finish(result: ExternalConversationFinish): Promise<void>;
9030
+ fail(error: unknown): Promise<void>;
9031
+ }
9032
+
8440
9033
  /**
8441
9034
  * In-memory per-session progress telemetry. Counts the live signals the
8442
9035
  * chat layer already observes — streamed deltas, wire pulses, heartbeats,
@@ -8524,6 +9117,17 @@ interface TaskBudgetSnapshot {
8524
9117
  inputTokens: number;
8525
9118
  }
8526
9119
 
9120
+ /**
9121
+ * The slice of `WorkspaceIndexManager` the reference parser needs. Narrowed
9122
+ * to a structural type so the dependency stays one-way — the indexer already
9123
+ * takes this manager, and importing its class here would close the loop.
9124
+ */
9125
+ interface WorkspaceFileSource {
9126
+ readFiles(projectId: string): Promise<Array<{
9127
+ path: string;
9128
+ }>>;
9129
+ }
9130
+
8527
9131
  type TurnMessageOrigin = 'direct-user' | 'question-answer' | 'cross-gezel' | 'background-nudge' | 'system';
8528
9132
  interface GateScriptDiagnostic {
8529
9133
  scriptName: string;
@@ -8820,10 +9424,12 @@ declare class ChatManager {
8820
9424
  */
8821
9425
  private readonly afterSessionIdle;
8822
9426
  /**
8823
- * One live async file handoff per sender/target/project/path. Models can
9427
+ * One live async file handoff per sender/target/project/path/intent. Models can
8824
9428
  * emit the same `message_gezel` call several times while the sender turn
8825
9429
  * is still active; joining that work prevents a parked-send burst from
8826
- * replaying the same assignment into the recipient session.
9430
+ * replaying the same assignment into the recipient session. A later
9431
+ * validator repair for a file that was originally missing is distinct work
9432
+ * and must never join the older create request.
8827
9433
  */
8828
9434
  private readonly inflightFileHandoffs;
8829
9435
  /** Set once {@link shutdown} starts so deferred watchdogs don't fire into a tearing-down manager. */
@@ -8993,6 +9599,7 @@ declare class ChatManager {
8993
9599
  private readonly activeMemoryExtractions;
8994
9600
  private readonly store;
8995
9601
  private readonly events;
9602
+ private readonly externalConversations;
8996
9603
  private readonly memory;
8997
9604
  private readonly historyManager?;
8998
9605
  private readonly catalog;
@@ -9064,6 +9671,22 @@ declare class ChatManager {
9064
9671
  */
9065
9672
  setContentIndex(index: ContentIndex): void;
9066
9673
  private contentIndexRef?;
9674
+ /**
9675
+ * Workspace file index, used to recognize workspace paths an assistant
9676
+ * reply named in prose. Injected by `service.ts` after both exist (the
9677
+ * indexer takes this manager as a dependency, so the wiring points both
9678
+ * ways). When unset — unit tests, and any project the indexer has not
9679
+ * reached — the reference parser sees artifacts only, which is exactly
9680
+ * its pre-workspace behavior.
9681
+ */
9682
+ setWorkspaceIndex(index: WorkspaceFileSource): void;
9683
+ private workspaceIndexRef?;
9684
+ /**
9685
+ * The project's indexed workspace paths. Reads the indexer's persisted
9686
+ * listing rather than walking the tree — a turn must never pay for a
9687
+ * recursive stat of a repo checkout just to linkify a filename.
9688
+ */
9689
+ private workspaceInventory;
9067
9690
  /**
9068
9691
  * Wire the Keurmeester supervision engine. Set by `service.ts` after
9069
9692
  * both exist (the KeurmeesterManager needs `oneShotCompletion`, so
@@ -9111,6 +9734,39 @@ declare class ChatManager {
9111
9734
  */
9112
9735
  private resolveEffectiveContextWindow;
9113
9736
  private resolveEffectiveSessionModel;
9737
+ /** The on-device model store backing a local provider, when one is wired. */
9738
+ private localModelStore;
9739
+ /** Dead pins already repinned this process, keyed `provider\0deadModelId`. */
9740
+ private readonly repinnedDeadDefaults;
9741
+ /**
9742
+ * Substitute an installed model for a local model id the engine cannot
9743
+ * serve.
9744
+ *
9745
+ * A `config.defaultModel` pin can name weights that never landed — first-run
9746
+ * pins the hardware recommendation *before* the download, so an abandoned
9747
+ * multi-gigabyte fetch leaves the install pointing at a phantom model. The
9748
+ * pooled-router path catches that with `ModelNotInstalledError`, but the
9749
+ * machine-engine path forwards whatever string it is handed straight to the
9750
+ * broker, which answers `model_not_loaded` on every turn including the
9751
+ * background one-shots. Chat is then dead until someone edits config.json by
9752
+ * hand, because the Settings picker that would fix it stays hidden while
9753
+ * fewer than two models are installed.
9754
+ *
9755
+ * Deliberately additive: with nothing installed to fall back to, the id
9756
+ * passes through unchanged so the existing downstream errors (and the
9757
+ * first-run download banner) behave exactly as before. We only intervene
9758
+ * when we have something strictly better to offer.
9759
+ */
9760
+ private ensureServableLocalModel;
9761
+ /**
9762
+ * Persist a substitution when the dead id was the install default, so the
9763
+ * repair outlives the turn and Settings, the engine pill, and the session
9764
+ * switcher stop disagreeing about which model is in play. A dead value that
9765
+ * came from gezel frontmatter or a capability route is substituted for this
9766
+ * turn but left in place — rewriting someone's explicit pin is a bigger
9767
+ * decision than keeping chat alive.
9768
+ */
9769
+ private repinDeadDefaultModel;
9114
9770
  /**
9115
9771
  * List the chat models a paired server offers, namespaced for A
9116
9772
  * (`remote:<remoteId>/<model>`). Used by the model picker. Returns [] if the
@@ -9205,10 +9861,19 @@ declare class ChatManager {
9205
9861
  projectId: string;
9206
9862
  createdAt: string;
9207
9863
  title: string;
9208
- providerName: "remote" | "llama-cpp" | "mlx" | "copilot" | "openai" | "anthropic" | "anthropic-cli" | "codex-cli" | "ollama" | "ds4";
9864
+ providerName: "mlx" | "ds4" | "llama-cpp" | "ollama" | "copilot" | "openai" | "anthropic" | "anthropic-cli" | "codex-cli" | "remote";
9209
9865
  lastActivityAt: string;
9210
9866
  taskRef?: string | undefined;
9211
9867
  stepId?: string | undefined;
9868
+ source?: {
9869
+ kind: "external";
9870
+ appId: string;
9871
+ appName: string;
9872
+ externalConversationId: string;
9873
+ readOnly: true;
9874
+ workingDirectory?: string | undefined;
9875
+ projectHint?: string | undefined;
9876
+ } | undefined;
9212
9877
  model?: string | undefined;
9213
9878
  archived?: boolean | undefined;
9214
9879
  lastTurnError?: string | undefined;
@@ -9460,6 +10125,13 @@ declare class ChatManager {
9460
10125
  */
9461
10126
  interruptWithMessage(sessionId: string, userText: string): Promise<ChatMessage>;
9462
10127
  getSessionRecord(sessionId: string): Promise<ChatSession | null>;
10128
+ /**
10129
+ * Begin one request in a caller-owned conversation loop (Pi, OpenCode, or VS Code).
10130
+ * The recorder reconciles the caller's authoritative transcript into a
10131
+ * read-only Gezel session and returns live-event hooks for this response.
10132
+ */
10133
+ beginExternalConversation(input: BeginExternalConversationInput): Promise<ExternalConversationTurn>;
10134
+ resolveExternalConversationId(input: ResolveExternalConversationInput): Promise<string>;
9463
10135
  /**
9464
10136
  * The set of tool names a session's active craftbook pre-authorizes
9465
10137
  * (its `autoAllow` toolsets' tools). The Claude-CLI permission broker
@@ -9488,11 +10160,10 @@ declare class ChatManager {
9488
10160
  * effort). Surfaced via `GET /api/sessions/:id/debug` and consumed
9489
10161
  * by the UI's debug-mode "copy debug bundle" button.
9490
10162
  *
9491
- * This re-runs `buildSessionOpts` rather than reading whatever was
9492
- * captured at session-creation time the *current* prompt is what
9493
- * matters when an engineer is investigating "why did the model do
9494
- * X right now". Recent edits to the gezel's about, project context,
9495
- * or local-model-tuning rules show up immediately.
10163
+ * Native sessions re-run `buildSessionOpts` because the *current* prompt
10164
+ * matters when investigating "why did the model do X right now". External
10165
+ * mirrors instead surface the latest captured caller-owned request; a native
10166
+ * Gezel prompt reconstructed later is not execution evidence for that loop.
9496
10167
  *
9497
10168
  * `atMessageTimestamp`: when set (ISO string from `ChatMessage.at`),
9498
10169
  * slice messages whose `at <= atMessageTimestamp` so the bundle
@@ -9511,8 +10182,26 @@ declare class ChatManager {
9511
10182
  * test teardown (or a graceful process exit) races them.
9512
10183
  */
9513
10184
  trackBackground(promise: Promise<unknown>): void;
9514
- /** Await every currently-tracked background promise, then return. */
9515
- drainBackground(): Promise<void>;
10185
+ /**
10186
+ * Await every currently-tracked background promise, then return.
10187
+ *
10188
+ * Bounded, because every caller is a shutdown path and the work being
10189
+ * awaited can legitimately take minutes. A first run creates the Meester
10190
+ * and kicks off its about.md + icon generation; if the configured provider
10191
+ * cannot answer — no credential, a cold cloud session — those one-shots sit
10192
+ * on their own 120s budget while this loop waits. Unbounded, that stalls
10193
+ * `service.stop()`, and neither the Electron quit coordinator nor an
10194
+ * embedded caller imposes a deadline of its own, so the window simply hangs.
10195
+ * Wild-caught as an intermittent 60s teardown timeout in
10196
+ * `security-compliance.spec.ts` once its home stopped inheriting a
10197
+ * machine-shared roster and started genuinely provisioning a Meester.
10198
+ *
10199
+ * Abandoning a task is safe here: these are fire-and-forget writes whose
10200
+ * own error handling already runs independently of this await. The warning
10201
+ * names the count so a real shutdown stall stays diagnosable instead of
10202
+ * looking like a clean exit.
10203
+ */
10204
+ drainBackground(timeoutMs?: number): Promise<void>;
9516
10205
  createSession(args: {
9517
10206
  gezelId: string;
9518
10207
  projectId?: string;
@@ -9692,6 +10381,8 @@ declare class ChatManager {
9692
10381
  toGezelIdOrName: string;
9693
10382
  projectId?: string;
9694
10383
  text: string;
10384
+ /** One-way feedback that must not wake the sender with the recipient's reply. */
10385
+ suppressReply?: boolean;
9695
10386
  /** Internal task context for scheduler re-drives. */
9696
10387
  taskRef?: string;
9697
10388
  /** Exact step paired with taskRef when re-driving task work. */
@@ -10359,9 +11050,22 @@ declare class ChatManager {
10359
11050
  * Tear down cached sessions, tool bridges, and provider engines so the
10360
11051
  * next session picks up new config.
10361
11052
  *
10362
- * Default (hard) mode tears down everything immediately correct for
10363
- * credential / provider / endpoint changes, where the old engine state
10364
- * is unusable and must go now.
11053
+ * Default (hard) mode disconnects every session and disposes every tool
11054
+ * bridge immediately — correct for credential / provider / endpoint /
11055
+ * security-boundary changes, where the old session state and the old MCP
11056
+ * children are unusable and must go now.
11057
+ *
11058
+ * What it does NOT do is kill resident local engines. A pooled engine is
11059
+ * a model process keyed by (provider, modelId, replica): it carries no
11060
+ * credentials and no tool surface, so no config change makes the *process*
11061
+ * unsafe — only the sessions and bridges around it. `engines: 'force'`
11062
+ * (service shutdown, emergency stop) force-evicts the pool without waiting
11063
+ * for in-flight turns; the default `'release-idle'` frees every engine
11064
+ * that is not mid-turn and leaves busy ones to the pool's ordinary
11065
+ * LRU/idle lifecycle. Wild-caught: flipping the security posture in
11066
+ * Settings force-killed both resident 27B models, and a background
11067
+ * craftbook turn four minutes into its first step died with "the
11068
+ * on-device engine dropped the connection", which reads as a crash.
10365
11069
  *
10366
11070
  * `deferBusy` mode is for pure model-preference changes (default chat
10367
11071
  * model, reasoning effort), where the only thing that needs to flow
@@ -10379,6 +11083,8 @@ declare class ChatManager {
10379
11083
  resetClient(opts?: {
10380
11084
  deferBusy?: boolean;
10381
11085
  restoreSeededProviders?: boolean;
11086
+ /** See method doc. Defaults to `'release-idle'` — never severs a live turn. */
11087
+ engines?: 'release-idle' | 'force';
10382
11088
  }): Promise<void>;
10383
11089
  /**
10384
11090
  * Stop admitting work and actively unwind every live turn while the HTTP
@@ -10478,6 +11184,8 @@ declare class ChatManager {
10478
11184
  * expansion) — those must keep today's plain-background behavior.
10479
11185
  */
10480
11186
  ambient?: boolean;
11187
+ /** Override the background default for explicitly user-awaited utility work. */
11188
+ lane?: Lane;
10481
11189
  /**
10482
11190
  * Live streaming hooks for callers that surface progress (the
10483
11191
  * transform dialog's metacommentary feed). `onDelta` is the
@@ -10565,6 +11273,12 @@ declare class ChatManager {
10565
11273
  * immediately). No-op when the session isn't mid-turn.
10566
11274
  */
10567
11275
  recordSessionIntent(scope: PublishScope, label: string): void;
11276
+ /**
11277
+ * External sessions never enter `states`, so the ordinary post-send memory
11278
+ * hook cannot see them. Re-read their durable transcript and use the same
11279
+ * cadence, ambient lane, cursor, and debounce policy as first-party chats.
11280
+ */
11281
+ private scheduleExternalMemoryExtraction;
10568
11282
  /**
10569
11283
  * Whether the post-turn memory extraction should fire now. The
10570
11284
  * cadence depends on how heavyweight the provider's one-shot path
@@ -10663,6 +11377,20 @@ declare class ChatManager {
10663
11377
  * a test seam) and is deliberately left alone.
10664
11378
  */
10665
11379
  private shutdownOwnedEngineRouter;
11380
+ /**
11381
+ * Free the memory a live reset can free without severing anything: evict
11382
+ * every resident engine that is idle right now and leave the rest to the
11383
+ * pool's ordinary lifecycle. The router itself stays — dropping it while an
11384
+ * engine is still serving a turn would orphan that process outside the
11385
+ * broker's accounting, which is the reason the old path force-killed
11386
+ * everything instead.
11387
+ *
11388
+ * An explicitly injected {@link engineRouter} remains caller-owned and is
11389
+ * left alone, same rule as {@link shutdownOwnedEngineRouter}. A router
11390
+ * whose construction is still in flight owns no resident engines yet, so
11391
+ * this deliberately does not await it.
11392
+ */
11393
+ private releaseIdleOwnedEngines;
10666
11394
  /**
10667
11395
  * Resolve (or build) the router. Returns `null` only when no local
10668
11396
  * provider is wired up at all (cloud-only installs); production
@@ -10889,8 +11617,17 @@ declare class ChatManager {
10889
11617
  * pool replica. This is the broker-side half of remote admission: focusing a
10890
11618
  * chat needs the live memory clamp so the user daemon can size its prompt,
10891
11619
  * but it must not load the model or evict somebody else's resident engine.
10892
- */
10893
- previewContextWindowForModel(name: LocalProviderName, modelId: string): Promise<number | undefined>;
11620
+ *
11621
+ * `standalone` is for callers writing the answer somewhere durable rather
11622
+ * than spending it on one turn. Live pricing charges the model for whatever
11623
+ * else happens to be resident, so a preview taken while another engine is
11624
+ * warm collapses to the local context floor — fine for a decision
11625
+ * made and discarded in the same second, wrong for a config file read back
11626
+ * days later. See {@link previewLocalEnginePlan}'s `standalone` note.
11627
+ */
11628
+ previewContextWindowForModel(name: LocalProviderName, modelId: string, opts?: {
11629
+ standalone?: boolean;
11630
+ }): Promise<number | undefined>;
10894
11631
  /**
10895
11632
  * Reservations held by models OTHER than the one being previewed.
10896
11633
  *
@@ -11022,6 +11759,20 @@ declare class ChatManager {
11022
11759
  * could do — so the launch config, not the catalog tag, is the source.
11023
11760
  */
11024
11761
  private resolveTurnVisionContext;
11762
+ /**
11763
+ * Shared-library documents bearing on THIS message.
11764
+ *
11765
+ * The turn-1 recall snapshot is frozen for the session (per the KV-cache
11766
+ * rationale on `record.recall`), so a topic that arrives later — "what's
11767
+ * our refund policy" on turn eight — would otherwise reach the model with
11768
+ * no retrieval at all. This runs on the user-message channel instead,
11769
+ * where per-turn content belongs.
11770
+ *
11771
+ * Quiet by construction: real user turns only, a high score floor, and a
11772
+ * per-session dedupe so the same document is offered once and then left
11773
+ * alone. No hits means no prelude and no tokens.
11774
+ */
11775
+ private resolveTurnLibraryRecall;
11025
11776
  private resolveUserPromptPrelude;
11026
11777
  /**
11027
11778
  * Get or build the live state for a session. On first use after process
@@ -12070,6 +12821,23 @@ declare class GitManager {
12070
12821
  private withLock;
12071
12822
  }
12072
12823
 
12824
+ type QuotaReserveRule = 'overall' | 'per-day';
12825
+ /** Why one bucket failed the reserve check (provider attached by the gate). */
12826
+ interface QuotaReserveViolation {
12827
+ /** Provider bucket id that tripped the reserve (e.g. "premium_interactions"). */
12828
+ bucket: string;
12829
+ /** Derived remaining, 0-100, one decimal. */
12830
+ remainingPercent: number;
12831
+ /** Effective floor that triggered the hold, 0-100, one decimal. */
12832
+ floorPercent: number;
12833
+ /** Which enabled rule produced the winning floor ('overall' on ties). */
12834
+ rule: QuotaReserveRule;
12835
+ resetDate?: string;
12836
+ }
12837
+ interface QuotaReserveHold extends QuotaReserveViolation {
12838
+ provider: ProviderName;
12839
+ }
12840
+
12073
12841
  /**
12074
12842
  * TaskRunner
12075
12843
  *
@@ -12154,8 +12922,12 @@ interface PendingHandoff {
12154
12922
  * activated during the day is filed as scheduled work immediately,
12155
12923
  * rather than counting as a backlog item for the up-to-one-tick window
12156
12924
  * before the first `tick` reads it back off disk.
12925
+ *
12926
+ * `'night-quota'` — the shift is ON but this handoff's resolved provider
12927
+ * is inside the configured cloud quota reserve; re-checked every tick so
12928
+ * a mid-window quota reset releases it.
12157
12929
  */
12158
- heldFor?: 'night-shift' | 'provider-busy';
12930
+ heldFor?: 'night-shift' | 'night-quota' | 'provider-busy';
12159
12931
  }
12160
12932
  /** One side of the pending split — see {@link TaskRunner.snapshot}. */
12161
12933
  interface TaskHandoffBucket {
@@ -12259,6 +13031,13 @@ interface TaskRunnerOptions {
12259
13031
  * the engine. Defaults to never-active.
12260
13032
  */
12261
13033
  isIndexCatchUpActive?: () => boolean;
13034
+ /**
13035
+ * Quota-reserve verdict for a night handoff's resolved provider. A
13036
+ * non-null hold keeps the handoff on the queue (`heldFor: 'night-quota'`)
13037
+ * without touching in-flight turns — admission-only, like every other
13038
+ * gate here. Defaults to always-allow.
13039
+ */
13040
+ nightQuotaHold?: (provider: ProviderName) => Promise<QuotaReserveHold | null>;
12262
13041
  }
12263
13042
  declare class TaskRunner {
12264
13043
  private readonly store;
@@ -12268,6 +13047,7 @@ declare class TaskRunner {
12268
13047
  private readonly isNightShiftActive;
12269
13048
  private readonly isNightShiftPending;
12270
13049
  private readonly isIndexCatchUpActive;
13050
+ private readonly nightQuotaHold;
12271
13051
  private readonly pending;
12272
13052
  private readonly activeDispatches;
12273
13053
  private nextId;
@@ -12335,6 +13115,11 @@ declare class TaskRunner {
12335
13115
  };
12336
13116
  /** Run one serialized dispatch pass now, sharing the interval's overlap guard. */
12337
13117
  wake(): Promise<void>;
13118
+ /**
13119
+ * Public test/operator entry point. Keep it serialized with the interval:
13120
+ * callers may drive a real, already-started service whose ticker is active.
13121
+ */
13122
+ tick(): Promise<void>;
12338
13123
  /**
12339
13124
  * Scan every `active` task and enqueue a handoff for any whose
12340
13125
  * current step has an effective gezel owner. Call on service boot and
@@ -12352,9 +13137,9 @@ declare class TaskRunner {
12352
13137
  /**
12353
13138
  * One dispatch pass. Walks pending in FIFO order; dispatches any
12354
13139
  * whose target provider has a free slot; skips items for tasks
12355
- * that are no longer active. Public for test-driven ticks.
13140
+ * that are no longer active.
12356
13141
  */
12357
- tick(): Promise<void>;
13142
+ private tickOnce;
12358
13143
  private pruneActiveDispatches;
12359
13144
  }
12360
13145
 
@@ -12395,6 +13180,14 @@ declare class WorkspaceIndexManager {
12395
13180
  /** Per-project promise chain so two concurrent scans on the same
12396
13181
  * project serialize rather than racing on the disk. */
12397
13182
  private readonly locks;
13183
+ /**
13184
+ * Parsed `files.json` per project, revalidated against the file's own
13185
+ * mtime + size so it can never go stale. `files.json` runs to megabytes
13186
+ * on a real checkout and its readers are all hot — terminal path
13187
+ * autocomplete on every keystroke, the chat reference parser on every
13188
+ * turn, the timeline backfill on every page.
13189
+ */
13190
+ private readonly filesCache;
12398
13191
  private startupTimer;
12399
13192
  private tickTimer;
12400
13193
  constructor(opts: WorkspaceIndexManagerOptions);
@@ -12432,7 +13225,7 @@ declare class WorkspaceIndexManager {
12432
13225
  * autocomplete. The index stores files only, so directories are derived:
12433
13226
  * each match collapses to its next path segment after the prefix — a
12434
13227
  * directory (trailing `/`, so the user can descend) or the full file path.
12435
- * Reads the cached file list (capped at ~500 by the indexer).
13228
+ * Reads the cached file list (capped at 20,000 files by the indexer).
12436
13229
  * Case-insensitive prefix; deduped; capped.
12437
13230
  */
12438
13231
  searchWorkspaceFiles(projectId: string, prefix: string, limit?: number): Promise<string[]>;
@@ -12478,6 +13271,7 @@ declare class WorkspaceIndexManager {
12478
13271
  private readMetaFromDisk;
12479
13272
  private isStale;
12480
13273
  private mruIndex;
13274
+ private isSharedLibrary;
12481
13275
  /**
12482
13276
  * Missing preserves the historical indexed-by-default behavior. Reads the
12483
13277
  * live project each time so toggling the setting takes effect without a
@@ -13077,6 +13871,7 @@ declare class IndexEnrichmentManager {
13077
13871
  * awaited) and then wakes the TaskRunner still gets the dispatch hold.
13078
13872
  */
13079
13873
  catchUpAll(): Promise<void>;
13874
+ private isSharedLibrary;
13080
13875
  private runDrive;
13081
13876
  /** Exposed for tests: run one batch ignoring the timers (still idle-gated). */
13082
13877
  tick(): Promise<void>;
@@ -13096,17 +13891,11 @@ interface SessionSearchHit {
13096
13891
  lastActivityAt: string;
13097
13892
  archived: boolean;
13098
13893
  }
13099
- interface DocumentSearchHit {
13100
- path: string;
13101
- lineStart: number;
13102
- snippet: string;
13103
- }
13104
13894
  interface GlobalIndexStatus {
13105
13895
  available: boolean;
13106
13896
  sessions: number;
13107
13897
  history: number;
13108
13898
  historyBackfilledAt: string | null;
13109
- documents: number;
13110
13899
  }
13111
13900
  declare class GlobalIndex {
13112
13901
  private readonly home;
@@ -13122,7 +13911,6 @@ declare class GlobalIndex {
13122
13911
  * falls back to the JSONL scan.
13123
13912
  */
13124
13913
  searchHistory(filter?: HistoryFilter): Promise<HistoryEvent[] | null>;
13125
- searchDocuments(q: string, maxResults?: number): Promise<DocumentSearchHit[]>;
13126
13914
  status(): Promise<GlobalIndexStatus>;
13127
13915
  }
13128
13916
 
@@ -13143,44 +13931,6 @@ declare class IndexingJobControl {
13143
13931
  private noteAuthor;
13144
13932
  }
13145
13933
 
13146
- interface ActivityTrackerOptions {
13147
- store: Store;
13148
- history: HistoryManager;
13149
- chatEvents: ChatEventBus;
13150
- now?: () => number;
13151
- persistThresholdMs?: number;
13152
- }
13153
- declare class ActivityTracker {
13154
- private readonly store;
13155
- private readonly history;
13156
- private readonly chatEvents;
13157
- private readonly now;
13158
- private readonly persistThresholdMs;
13159
- /** Newest observed stamp per project (ms epoch). */
13160
- private readonly latest;
13161
- /** Stamp last written to (or read from) disk per project. */
13162
- private readonly persisted;
13163
- /** Projects whose on-disk stamp has been loaded into `persisted`. */
13164
- private readonly loads;
13165
- private readonly writing;
13166
- private unsubs;
13167
- constructor(opts: ActivityTrackerOptions);
13168
- start(): void;
13169
- stop(): Promise<void>;
13170
- stamp(projectId: string, atMs: number): void;
13171
- /**
13172
- * Newest known activity for a project — max of the in-memory stamp
13173
- * and the on-disk one (loaded once). Null when the project has never
13174
- * been observed.
13175
- */
13176
- lastActivityAt(projectId: string): Promise<string | null>;
13177
- /** Persist every stamp that is newer than its on-disk value. */
13178
- flush(): Promise<void>;
13179
- private maybePersist;
13180
- private write;
13181
- private ensureLoaded;
13182
- }
13183
-
13184
13934
  type StatusOneShot = (prompt: string, timeoutMs: number, opts: {
13185
13935
  gezelId: string;
13186
13936
  jobLabel: string;
@@ -13417,6 +14167,8 @@ declare class ChatModelInstallRegistry<E extends ChatInstallEventBase, O> {
13417
14167
  interface GgufInstallOpts {
13418
14168
  skipSha: boolean;
13419
14169
  includeMmproj: boolean;
14170
+ /** Whether a text-only chat install should also pull the default image reader. */
14171
+ installCompanion: boolean;
13420
14172
  }
13421
14173
  interface MlxInstallOpts {
13422
14174
  skipSha: boolean;
@@ -13429,6 +14181,26 @@ interface ChatModelInstallRegistries {
13429
14181
  mlx: MlxChatInstallRegistry;
13430
14182
  }
13431
14183
 
14184
+ interface OpenCodeSetupManager {
14185
+ status(): Promise<OpenCodeSetupStatusResponse>;
14186
+ configure(input: ConfigureOpenCodeRequest): Promise<OpenCodeSetupStatusResponse>;
14187
+ installPlugin(input: InstallOpenCodePluginRequest): Promise<OpenCodeSetupStatusResponse>;
14188
+ removePlugin(): Promise<OpenCodeSetupStatusResponse>;
14189
+ remove(): Promise<OpenCodeSetupStatusResponse>;
14190
+ reconcile(): Promise<void>;
14191
+ stop(): Promise<void>;
14192
+ }
14193
+
14194
+ interface PiSetupManager {
14195
+ status(): Promise<PiSetupStatusResponse>;
14196
+ configure(input: ConfigurePiRequest): Promise<PiSetupStatusResponse>;
14197
+ installExtension(input: InstallPiExtensionRequest): Promise<PiSetupStatusResponse>;
14198
+ removeExtension(): Promise<PiSetupStatusResponse>;
14199
+ remove(): Promise<PiSetupStatusResponse>;
14200
+ reconcile(): Promise<void>;
14201
+ stop(): Promise<void>;
14202
+ }
14203
+
13432
14204
  /**
13433
14205
  * AudioProvider — service-side abstractions for speech-to-text (STT)
13434
14206
  * and text-to-speech (TTS).
@@ -14135,6 +14907,11 @@ interface InstalledVideoModelInfo {
14135
14907
  name: string;
14136
14908
  approxSizeBytes: number;
14137
14909
  installedAt: string;
14910
+ /**
14911
+ * True when the model lives in the read-only machine asset store, so
14912
+ * `deleteModel` would refuse it and the UI must not offer Delete.
14913
+ */
14914
+ readOnly?: boolean;
14138
14915
  }
14139
14916
  /**
14140
14917
  * Multi-file pull progress. Mirrors `MlxInstallEvent` (video models are
@@ -14582,6 +15359,36 @@ declare class SearchService {
14582
15359
  private merge;
14583
15360
  }
14584
15361
 
15362
+ /**
15363
+ * Tracks the one storage operation allowed to run at a time.
15364
+ *
15365
+ * Cleanup, backup, and restore all walk and rewrite the same directories, so
15366
+ * running two at once means one job measuring or archiving a tree the other
15367
+ * is deleting. The HTTP layer rejects a second start rather than trying to
15368
+ * interleave them, matching how the folder-move worker guards itself.
15369
+ */
15370
+ declare class StorageJobManager {
15371
+ private readonly jobs;
15372
+ create(kind: StorageJobKind, totals?: {
15373
+ totalItems?: number;
15374
+ totalBytes?: number;
15375
+ }): StorageJob;
15376
+ get(id: string): StorageJob | undefined;
15377
+ hasActive(): boolean;
15378
+ /**
15379
+ * Ask the worker to stop at its next item boundary. Deletion is not undone
15380
+ * — whatever already went is gone — so this bounds the damage rather than
15381
+ * reversing it, and the job still reports what it freed.
15382
+ */
15383
+ requestCancel(id: string): boolean;
15384
+ update(id: string, patch: Partial<StorageJob>): void;
15385
+ setPhase(id: string, phase: StorageJobPhase, currentLabel?: string): void;
15386
+ finish(id: string, outcome: {
15387
+ error?: string;
15388
+ cancelled?: boolean;
15389
+ }): void;
15390
+ }
15391
+
14585
15392
  /**
14586
15393
  * The pinned system-toolset list.
14587
15394
  *
@@ -14870,6 +15677,24 @@ interface NightShiftManagerOptions {
14870
15677
  intervalMs?: number;
14871
15678
  /** Clock override for tests. */
14872
15679
  now?: () => Date;
15680
+ /** Quota-reserve verdicts (NightShiftQuotaGate); absent = never hold. */
15681
+ quotaGate?: {
15682
+ holdFor(provider: ProviderName): Promise<QuotaReserveHold | null>;
15683
+ };
15684
+ /**
15685
+ * Night-aware provider resolution — the same `chat.providerForGezel`
15686
+ * closure the runner's dispatcher uses, so classification here and
15687
+ * admission there cannot disagree. Absent = the gate never applies.
15688
+ */
15689
+ resolveProviderName?: (gezelId: string, opts?: {
15690
+ nightShift?: boolean;
15691
+ }) => Promise<ProviderName>;
15692
+ }
15693
+ /** Why pending night work is parked, summarized for status surfaces. */
15694
+ interface NightShiftQuotaHoldStatus {
15695
+ heldTaskCount: number;
15696
+ /** One reason per affected provider. */
15697
+ reasons: QuotaReserveHold[];
14873
15698
  }
14874
15699
  /**
14875
15700
  * Owns the Night Shift ON/OFF state. Lifecycle mirrors `TaskScheduler` /
@@ -14883,6 +15708,12 @@ interface NightShiftManagerOptions {
14883
15708
  * pending night-shift tasks remain, then reverts to scheduled logic.
14884
15709
  * - SCHEDULED: outside the window → off (latch cleared); window open but
14885
15710
  * no pending work → latch off for the rest of THIS window; otherwise on.
15711
+ * - QUOTA-HELD: pending work whose resolved provider is inside the cloud
15712
+ * quota reserve doesn't keep a shift alive. All pending work held →
15713
+ * off WITHOUT latching (parked, not drained) so a mid-window quota
15714
+ * reset re-activates on a later tick; a manual request survives the
15715
+ * hold and resumes by itself. `quotaHoldStatus()` names the parked
15716
+ * state for the UI.
14886
15717
  *
14887
15718
  * Consumers read `isActive()` synchronously: `TaskRunner` (dispatch gating
14888
15719
  * + priority), `TaskScheduler` (cron-spawn gating), `IndexEnrichmentManager`
@@ -14894,10 +15725,14 @@ declare class NightShiftManager {
14894
15725
  private readonly events;
14895
15726
  private readonly intervalMs;
14896
15727
  private readonly now;
15728
+ private readonly quotaGate?;
15729
+ private readonly resolveProviderName?;
14897
15730
  private timer;
14898
15731
  private ticking;
14899
15732
  private active;
14900
15733
  private src;
15734
+ /** When the running shift began; null while nothing is running. */
15735
+ private startedAt;
14901
15736
  /** Window key we've drained-and-latched off for; cleared next window. */
14902
15737
  private latchedOffForWindowKey;
14903
15738
  /** Whether a manual ("go to lunch") shift has been requested. */
@@ -14910,11 +15745,44 @@ declare class NightShiftManager {
14910
15745
  private enabled;
14911
15746
  /** Window config from the last tick — drives the synchronous day-key. */
14912
15747
  private window;
15748
+ /** Quota-reserve hold summary from the last tick; null when nothing is held. */
15749
+ private quotaHold;
15750
+ /** Refs of pending night tasks the quota reserve is holding, per last tick. */
15751
+ private quotaHeldRefs;
14913
15752
  constructor(opts: NightShiftManagerOptions);
14914
15753
  start(): void;
14915
15754
  stop(): void;
14916
15755
  isActive(): boolean;
14917
15756
  source(): NightShiftSource;
15757
+ /**
15758
+ * When the running shift began, or null when nothing is running. This is
15759
+ * the ON edge, not the window's opening hour: a scheduled shift starts on
15760
+ * the tick that turned it on, which is later than `startHour` whenever the
15761
+ * machine was asleep or the work only arrived mid-window. A shift that
15762
+ * changes source mid-run (a scheduled window taking over from a manual
15763
+ * one) keeps its original start — it never stopped running.
15764
+ */
15765
+ startedAtIso(): string | null;
15766
+ /**
15767
+ * The scheduled window status surfaces name: the one open right now, else
15768
+ * the next one due. Null while the feature is switched off — nothing is
15769
+ * scheduled, so there is no window to name. Uses the window config cached
15770
+ * from the last tick.
15771
+ */
15772
+ windowBounds(): {
15773
+ start: string;
15774
+ end: string;
15775
+ open: boolean;
15776
+ } | null;
15777
+ /**
15778
+ * Why pending night work is parked by the cloud quota reserve, as of the
15779
+ * last tick — null when nothing is held. Non-null both while the shift
15780
+ * runs with a subset held (mixed providers) and while it sits fully
15781
+ * parked (active=false, no latch) waiting for a quota window to reset.
15782
+ */
15783
+ quotaHoldStatus(): NightShiftQuotaHoldStatus | null;
15784
+ /** Refs of the pending night tasks currently held by the quota reserve. */
15785
+ quotaHeldTaskRefs(): ReadonlySet<string>;
14918
15786
  /**
14919
15787
  * Whether the configured nightly window is open right now — used by the
14920
15788
  * scheduler to confine a night-shift cron host's spawning to the window
@@ -14985,8 +15853,15 @@ declare class NightShiftManager {
14985
15853
  * exposes the same set for the UI's "what's the shift doing?" panel.
14986
15854
  */
14987
15855
  listPendingTasks(): Promise<Task[]>;
14988
- /** True if any active night-shift task still has work to do `todayKey`. */
14989
- private hasPendingNightShiftTasks;
15856
+ /**
15857
+ * Partition pending night tasks into dispatchable vs quota-held using
15858
+ * the same provider resolution the runner's dispatcher applies (night
15859
+ * override included). Optimistic on every failure path: a task whose
15860
+ * owner or provider can't resolve counts dispatchable — the runner
15861
+ * drops it later, and counting it held would wrongly deactivate the
15862
+ * shift. Verdicts are memoized per provider per pass.
15863
+ */
15864
+ private classifyPending;
14990
15865
  /** Active night-shift tasks pending `todayKey`, in `manager.list` order. */
14991
15866
  private pendingNightShiftTasks;
14992
15867
  /** Single transition chokepoint: diff, broadcast, log. */
@@ -15499,6 +16374,14 @@ declare class TerminalManager {
15499
16374
  private collectListingFileReferences;
15500
16375
  }
15501
16376
 
16377
+ interface VSCodeSetupManager {
16378
+ status(): Promise<VSCodeSetupStatusResponse>;
16379
+ configure(input: ConfigureVSCodeRequest): Promise<VSCodeSetupStatusResponse>;
16380
+ remove(): Promise<VSCodeSetupStatusResponse>;
16381
+ reconcile(): Promise<void>;
16382
+ stop(): Promise<void>;
16383
+ }
16384
+
15502
16385
  interface OllamaEmulationStatus {
15503
16386
  listening: boolean;
15504
16387
  port?: number;
@@ -15540,6 +16423,12 @@ interface ServiceContext {
15540
16423
  * manual run.
15541
16424
  */
15542
16425
  meesterStatus: MeesterStatusGenerator;
16426
+ /**
16427
+ * The meester's ambient dashboard (PNG workshop snapshots under
16428
+ * `~/.gezel/ambient/`). In context so `/api/ambient-dashboard` can
16429
+ * report status and kick manual runs.
16430
+ */
16431
+ ambientDashboard: AmbientDashboardGenerator;
15543
16432
  scriptRunner: ScriptRunner;
15544
16433
  catalog: CatalogService;
15545
16434
  /**
@@ -15703,6 +16592,12 @@ interface ServiceContext {
15703
16592
  ollamaEmulation: OllamaEmulationController;
15704
16593
  /** Gezel-owned Codex profile, credential, and loopback bridge lifecycle. */
15705
16594
  codexSetup: CodexSetupManager;
16595
+ /** Gezel-owned OpenCode config, credential, and loopback bridge lifecycle. */
16596
+ opencodeSetup: OpenCodeSetupManager;
16597
+ /** Gezel-owned pi extension, model list, credential, and loopback bridge lifecycle. */
16598
+ piSetup: PiSetupManager;
16599
+ /** VS Code custom endpoint, scoped credential, profile merge, and bridge lifecycle. */
16600
+ vscodeSetup: VSCodeSetupManager;
15706
16601
  /**
15707
16602
  * Hex SHA-256 of the daemon's current TLS cert DER, when serving HTTPS.
15708
16603
  * `/v1/identity` signs this with the device identity key so a paired client
@@ -15733,6 +16628,13 @@ interface ServiceContext {
15733
16628
  * (the worker writes a sentinel file so the next boot can detect a
15734
16629
  * crashed mid-move). */
15735
16630
  folderJobs: JobManager;
16631
+ /** In-memory tracker for the one storage cleanup/backup/restore job that
16632
+ * may run at a time. Same lifetime as `folderJobs`, and mutually
16633
+ * exclusive with it — both rewrite the same directories. */
16634
+ storageJobs: StorageJobManager;
16635
+ /** Drop the cached model inventory after cleanup deletes model files, so
16636
+ * listings stop advertising models that are no longer on disk. */
16637
+ invalidateModelsCache?: (provider?: ProviderName) => void;
15736
16638
  /** Background workspace indexer: commands + files + token index. */
15737
16639
  workspaceIndex: WorkspaceIndexManager;
15738
16640
  /** Content index (code/doc intelligence) backing the code-intel MCP tools. */
@@ -15837,6 +16739,16 @@ interface StartServiceOptions {
15837
16739
  codexHome?: string;
15838
16740
  /** Exact Codex bridge port override (`0` = ephemeral); production derives one from `home`. */
15839
16741
  codexBridgePort?: number;
16742
+ /** Exact OpenCode bridge port override (`0` = ephemeral); production derives one from `home`. */
16743
+ opencodeBridgePort?: number;
16744
+ /** Override pi's fixed loopback bridge port. Tests bind ephemeral ports. */
16745
+ piBridgePort?: number;
16746
+ /** Override pi's own agent directory. Tests must never write to a real one. */
16747
+ piAgentDir?: string;
16748
+ /** Override VS Code's fixed loopback bridge port. Tests bind ephemeral ports. */
16749
+ vscodeBridgePort?: number;
16750
+ /** Override VS Code's User/profile directory. Tests must never write to a real one. */
16751
+ vscodeUserDir?: string;
15840
16752
  }
15841
16753
  interface RunningService {
15842
16754
  context: ServiceContext;