@bendyline/gezel-service 1.0.5 → 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 +16725 -6029
  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 +3 -6
  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 +1154 -252
  11. package/dist/index.js +16718 -6022
  12. package/dist/providers/mlx/python/gezel_mlx_server.py +126 -11
  13. package/dist/ui/assets/{TerminalCodeEditor-C_Mvaxqz.js → TerminalCodeEditor-BUHpk3pJ.js} +2 -2
  14. package/dist/ui/assets/{_.contribution-fOcpgyNy.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-Cx5HvoW7.js → cpp.contribution-CvG4GFxL.js} +1 -1
  20. package/dist/ui/assets/{csharp.contribution-DMuCSvzf.js → csharp.contribution-DekWAPj3.js} +1 -1
  21. package/dist/ui/assets/{css.contribution-BPXcRoVK.js → css.contribution-CDuF4Xjo.js} +1 -1
  22. package/dist/ui/assets/{cssMode-BHAnHChA.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-DQAHazBI.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-Jq5pYI7r.js → go.contribution-D_1myt_B.js} +1 -1
  31. package/dist/ui/assets/{handlebars-BWC7ZXbe.js → handlebars-Mxcy9PWv.js} +1 -1
  32. package/dist/ui/assets/{handlebars.contribution-BSmxDPeT.js → handlebars.contribution-DYF5z0XX.js} +2 -2
  33. package/dist/ui/assets/{html-BzC9B_Os.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-CEvtdKHG.js → html.contribution-C0xFTPDL.js} +2 -2
  36. package/dist/ui/assets/{htmlMode-1qtmM5J4.js → htmlMode-BVtBWJQM.js} +1 -1
  37. package/dist/ui/assets/{index-JGn32C5f.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-BQpzdQzE.js → ini.contribution-BqUQ8Oa0.js} +1 -1
  41. package/dist/ui/assets/{java.contribution-r3T-8LYE.js → java.contribution-u4QvI2UZ.js} +1 -1
  42. package/dist/ui/assets/{javascript-DlipM3VT.js → javascript-BVm8fvMk.js} +1 -1
  43. package/dist/ui/assets/javascript.contribution-BtlENuoy.js +2 -0
  44. package/dist/ui/assets/{jsonMode-DSXTaEG9.js → jsonMode-DcoSB4Jg.js} +1 -1
  45. package/dist/ui/assets/{kotlin.contribution-NI_YvQPr.js → kotlin.contribution-D1ZfW5lW.js} +1 -1
  46. package/dist/ui/assets/{less.contribution-BDz7g6GS.js → less.contribution-BBWek5Gc.js} +1 -1
  47. package/dist/ui/assets/{lua.contribution-Cr_f17YF.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-CeJFrLN_.js → markdown.contribution-CsVCTE0K.js} +1 -1
  50. package/dist/ui/assets/{monaco-Bv9TxWl_.js → monaco-DcZAEsIZ.js} +3 -3
  51. package/dist/ui/assets/{monaco-base-DD1Wr24S.js → monaco-base-C-UMfHa9.js} +1 -1
  52. package/dist/ui/assets/{monaco-setup-CSa3_fbr.js → monaco-setup-Dy_kv6Gv.js} +1 -1
  53. package/dist/ui/assets/{monaco.contribution-tTvEzZ5l.js → monaco.contribution-BpJqoa65.js} +2 -2
  54. package/dist/ui/assets/{monaco.contribution-QFH9z5g8.js → monaco.contribution-BujOhotZ.js} +2 -2
  55. package/dist/ui/assets/{monaco.contribution-ZD-wInG3.js → monaco.contribution-DW4-UIJp.js} +2 -2
  56. package/dist/ui/assets/{monaco.contribution-DKCJArGP.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-B310cs3S.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-CcCNDnkj.js → python-CCiWacbd.js} +1 -1
  61. package/dist/ui/assets/{python.contribution-ZHL594qQ.js → python.contribution-3CThbaoI.js} +2 -2
  62. package/dist/ui/assets/{ruby.contribution-CRsX2xPw.js → ruby.contribution-D3g6EADN.js} +1 -1
  63. package/dist/ui/assets/{rust.contribution-CfwWiQxD.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-CJaLNKeq.js → scss.contribution-mQ_vuYxE.js} +1 -1
  66. package/dist/ui/assets/{shell.contribution-DLPinQHD.js → shell.contribution-BLQSLx3H.js} +1 -1
  67. package/dist/ui/assets/{sql.contribution-DsZSLfVg.js → sql.contribution-GScL7Mrt.js} +1 -1
  68. package/dist/ui/assets/{swift.contribution-DFBOOFKs.js → swift.contribution-DG__yqNr.js} +1 -1
  69. package/dist/ui/assets/{terminal-monaco-setup-B8Xv2NV9.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-DqT2Dz4k.js → tsMode-B5nM5MR7.js} +1 -1
  72. package/dist/ui/assets/{typescript-_R5MS0Lp.js → typescript-xiOVha1M.js} +1 -1
  73. package/dist/ui/assets/{typescript.contribution-CHnXQvfG.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-CQVIKNxb.js → xml-m3MSnvuD.js} +1 -1
  77. package/dist/ui/assets/{xml.contribution-HY4GvlWC.js → xml.contribution-bOoaS55N.js} +2 -2
  78. package/dist/ui/assets/{yaml-Nt3uLkWC.js → yaml-DOtMVgbb.js} +1 -1
  79. package/dist/ui/assets/{yaml.contribution-C7dllTyY.js → yaml.contribution-CNCYEtwk.js} +2 -2
  80. package/dist/ui/index.html +14 -14
  81. package/package.json +10 -10
  82. package/dist/ui/assets/dist-DRDFuw7G.js +0 -222
  83. package/dist/ui/assets/dist-DYn91eSO.js +0 -2
  84. package/dist/ui/assets/index-DTicLc4o.css +0 -1
  85. package/dist/ui/assets/javascript.contribution-D4MU3K0o.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:
@@ -1762,118 +2091,26 @@ interface SendOptions {
1762
2091
  */
1763
2092
  declare class ChannelManager {
1764
2093
  private readonly store;
1765
- private readonly secrets;
1766
- private readonly history;
1767
- private readonly debug;
1768
- 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
- * If a send truly cannot resolve its session record, it does not know the
1812
- * gezelId/projectId and publishes session-scoped via `publishSessionOnly`.
1813
- * Once a record is known, even provider-start failures use `publish` so
1814
- * project/global clients receive the error.
1815
- */
1816
- declare class ChatEventBus {
1817
- private readonly sessionBuses;
1818
- private readonly projectListeners;
1819
- private readonly gezelListeners;
1820
- private readonly globalListeners;
1821
- publish(scope: PublishScope, event: ChatEvent): void;
1822
- /**
1823
- * Publish to the session bus only. Used when the manager's early-fail path
1824
- * doesn't know the gezel/project to envelope (the session record itself
1825
- * couldn't be loaded). Project + global subscribers never see these.
1826
- */
1827
- publishSessionOnly(sessionId: string, event: ChatEvent): void;
1828
- /**
1829
- * Publish a project-scoped lifecycle event that has no originating
1830
- * turn — e.g. `project_created`. Fans out to the project's listeners
1831
- * and all global listeners, but is deliberately NOT recorded in any
1832
- * session history: these are one-shot signals, not part of a
1833
- * replayable transcript, so a subscriber that reconnects later must
1834
- * not see them re-fired (unlike `publish`, which seeds `deliverToSession`
1835
- * history that replays until the turn's `done`). `sessionId`/`gezelId`
1836
- * are empty — the consumers that care (the sidebar refresh bridge)
1837
- * key off the event payload, not the envelope scope.
1838
- */
1839
- publishProjectEvent(projectId: string, event: ChatEvent): void;
1840
- /**
1841
- * Publish a project-less global lifecycle event — e.g. `night_shift`.
1842
- * Reaches only the global listeners (the Meester timeline SSE the UI
1843
- * subscribes to via `subscribeAll`); like `publishProjectEvent` it is
1844
- * not recorded in any session history, so reconnecting subscribers do
1845
- * not see it re-fired. The envelope scope fields are all empty;
1846
- * consumers key off the event payload.
1847
- */
1848
- publishGlobalEvent(event: ChatEvent): void;
1849
- subscribe(sessionId: string, listener: SessionListener): () => void;
1850
- subscribeProject(projectId: string, listener: EnvelopeListener): () => void;
1851
- subscribeGezel(gezelId: string, listener: EnvelopeListener): () => void;
1852
- subscribeAll(listener: EnvelopeListener): () => void;
1853
- private deliverToSession;
1854
- /**
1855
- * Append an event to a session's replay history, keeping the buffer
1856
- * lossless for the content that matters. Two rules:
1857
- *
1858
- * 1. Transient status pulses (`heartbeat`, `wire_pulse`) are delivered
1859
- * live but never recorded — they carry no replayable content, and a
1860
- * reasoning-heavy local-model turn emits enough of them to evict
1861
- * real deltas out of the 500-event cap.
1862
- * 2. Adjacent `delta` events coalesce into one entry. A five-minute
1863
- * thinking turn streams thousands of per-token deltas; stored
1864
- * individually they blow past the cap and the earliest thinking is
1865
- * silently dropped on replay. Merged, a text run costs one slot no
1866
- * matter how long it streamed. The stored entry is a fresh object —
1867
- * the original event was already handed to live listeners and must
1868
- * not be mutated.
2094
+ private readonly secrets;
2095
+ private readonly history;
2096
+ private readonly debug;
2097
+ private readonly channels;
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.
1869
2106
  */
1870
- private recordInHistory;
1871
- private ensureSession;
1872
- private cleanupSession;
1873
- private pruneIdleSessions;
1874
- private safeDeliver;
1875
- /** Lightweight observability hook used by tests and diagnostics. */
1876
- 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;
1877
2114
  }
1878
2115
 
1879
2116
  /**
@@ -1986,6 +2223,18 @@ type HookRunner = (hook: ActiveHook, ctx: {
1986
2223
  type McpServerSpec = StdioMcpServerSpec | HttpMcpServerSpec;
1987
2224
  interface StdioMcpServerSpec {
1988
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;
1989
2238
  command: string;
1990
2239
  args: string[];
1991
2240
  env: Record<string, string>;
@@ -1994,6 +2243,8 @@ interface StdioMcpServerSpec {
1994
2243
  }
1995
2244
  interface HttpMcpServerSpec {
1996
2245
  kind: 'http';
2246
+ /** Catalog toolset id — see {@link StdioMcpServerSpec.toolsetId}. */
2247
+ toolsetId?: string;
1997
2248
  /**
1998
2249
  * `streamable-http` is the current MCP HTTP transport (one URL,
1999
2250
  * POST for messages, optional GET for SSE stream + auto-reconnect).
@@ -2282,6 +2533,20 @@ interface PromptCtx extends ModelCtx {
2282
2533
  isMeester: boolean;
2283
2534
  /** The gezel's about.md content (already loaded). */
2284
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>;
2285
2550
  }
2286
2551
  /**
2287
2552
  * Context for per-turn hooks: {@link Behavior.userPromptPrelude} and
@@ -2311,6 +2576,17 @@ interface TurnCtx extends ModelCtx {
2311
2576
  assistantContent: string;
2312
2577
  /** How many continuation nudges have already fired in this user-initiated send. */
2313
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
+ }>;
2314
2590
  }
2315
2591
  /**
2316
2592
  * Verdict returned by {@link Behavior.postTurnDetector} when a
@@ -2655,6 +2931,18 @@ interface QuotaBucket {
2655
2931
  overage: number;
2656
2932
  resetDate?: string;
2657
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
+ }
2658
2946
  interface ProviderUsage {
2659
2947
  /** Non-empty only for providers that report account quota windows. */
2660
2948
  quotaBuckets: QuotaBucket[];
@@ -2685,8 +2973,30 @@ interface ProviderUsage {
2685
2973
  * "n/a" rather than zero.
2686
2974
  */
2687
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;
2688
2990
  lastUpdated: string | null;
2689
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
+ }
2690
3000
  interface UsageSummary {
2691
3001
  providers: {
2692
3002
  copilot?: ProviderUsage;
@@ -2715,6 +3025,8 @@ declare class UsageTracker {
2715
3025
  * those windows without inventing a zero-token turn or skewing totals.
2716
3026
  */
2717
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[];
2718
3030
  summary(): UsageSummary;
2719
3031
  private ensure;
2720
3032
  }
@@ -2767,7 +3079,7 @@ type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number];
2767
3079
  * — no wasted work. Already-running entries abort normally via the
2768
3080
  * same signal.
2769
3081
  *
2770
- * ## Ambient admission control (single-lane engine protection)
3082
+ * ## Ambient admission control (foreground-capacity protection)
2771
3083
  *
2772
3084
  * On a local engine, a housekeeping turn (scheduler nudge, memory
2773
3085
  * extraction, icon/about generation) occupies the GPU for minutes.
@@ -2777,17 +3089,19 @@ type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number];
2777
3089
  * engine is idle while the user READS, ambient work sneaks in, and
2778
3090
  * the user's move then waits out a 3-minute nudge turn).
2779
3091
  *
2780
- * When `ambientQuietMs > 0`, entries marked `ambient: true` dispatch
2781
- * only when BOTH hold: no non-ambient work is running or pending, and
2782
- * no non-ambient activity (enqueue, dispatch, or release, any lane)
2783
- * happened within the last `ambientQuietMs`. Reading time counts as
2784
- * engagement: page reactions and tool-loop turns are non-ambient, so
2785
- * an active game holds the gate closed between moves. Ambient entries
2786
- * are exempt from the anti-starvation guard while gated deferring
2787
- * them indefinitely during active use is the feature, not a bug; they
2788
- * run when the user steps away. The gate never touches non-ambient
2789
- * work, so mid-turn compaction (which a foreground turn awaits) and
2790
- * 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.
2791
3105
  */
2792
3106
  type Lane = 'interactive' | 'background';
2793
3107
  interface EnqueueRequest {
@@ -2796,9 +3110,10 @@ interface EnqueueRequest {
2796
3110
  * Marks truly-deferrable housekeeping (scheduler nudges, memory
2797
3111
  * extraction, icon/about generation, digests, night-shift chores).
2798
3112
  * When the queue was built with {@link ProviderQueueOptions.ambientQuietMs},
2799
- * ambient entries dispatch only after a quiet window with no
2800
- * NON-ambient activity — see "Ambient admission control" in the
2801
- * 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
2802
3117
  * covers anything that shouldn't cut in front of a typed chat turn,
2803
3118
  * which includes user-facing work (checkers page reactions, mid-turn
2804
3119
  * compaction a foreground turn awaits) that must NEVER be held.
@@ -2905,10 +3220,12 @@ interface ProviderQueueOptions {
2905
3220
  maxWaitMs?: number;
2906
3221
  /**
2907
3222
  * Quiet window for ambient admission control — see the class header.
2908
- * Entries marked `ambient: true` dispatch only after this many ms
2909
- * with no non-ambient activity (and none running or pending). 0 or
2910
- * unset disables the gate (ambient entries behave like plain
2911
- * 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
2912
3229
  * {@link defaultAmbientQuietMs}; cloud queues leave it off.
2913
3230
  */
2914
3231
  ambientQuietMs?: number;
@@ -2974,6 +3291,14 @@ declare class ProviderQueue {
2974
3291
  */
2975
3292
  describe(): {
2976
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;
2977
3302
  queuedInteractive: number;
2978
3303
  queuedBackground: number;
2979
3304
  /** Pending ambient entries currently held by the admission gate. */
@@ -3037,9 +3362,15 @@ declare class ProviderQueue {
3037
3362
  */
3038
3363
  private maybeScheduleAmbientRecheck;
3039
3364
  /**
3040
- * True when ambient entries may dispatch: gate disabled, or no
3041
- * non-ambient work anywhere on the queue AND the quiet window has
3042
- * 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.
3043
3374
  */
3044
3375
  private ambientGateOpen;
3045
3376
  /**
@@ -3817,9 +4148,10 @@ interface LLMSession {
3817
4148
  onDelta(handler: (chunk: string) => void): () => void;
3818
4149
  /**
3819
4150
  * Subscribe to live private-reasoning deltas, streamed separately from
3820
- * the visible reply so they never enter the committed body or the
3821
- * external API-compat forwarders. Optional only providers with a
3822
- * 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.
3823
4155
  */
3824
4156
  onReasoningDelta?(handler: (chunk: string) => void): () => void;
3825
4157
  onUsage(handler: (usage: TurnUsage) => void): () => void;
@@ -3844,7 +4176,7 @@ interface LLMSession {
3844
4176
  * calls arrive whole (Ollama) or run server-side (Copilot, OpenAI)
3845
4177
  * leave it undefined.
3846
4178
  */
3847
- onToolArgsDelta?(handler: (name: string, chunk: string) => void): () => void;
4179
+ onToolArgsDelta?(handler: (name: string, chunk: string, meta?: ToolArgsDeltaMeta) => void): () => void;
3848
4180
  /**
3849
4181
  * Optional: subscribe to phase-announcement events. Currently only
3850
4182
  * `CopilotSession` emits these (SDK `assistant.intent` events from
@@ -3971,6 +4303,13 @@ interface ExternalToolCall {
3971
4303
  name: string;
3972
4304
  arguments: string;
3973
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
+ }
3974
4313
  /**
3975
4314
  * Context describing one would-be batch member, passed to
3976
4315
  * {@link BatchCapability.admit} so an engine can make a live headroom
@@ -4399,11 +4738,14 @@ declare class CapacityBroker {
4399
4738
  /**
4400
4739
  * Fallback estimate when a catalog entry lacks `residentBytes`.
4401
4740
  * Used by callers building reservation requests from
4402
- * `approxSizeBytes`. Multipliers reflect typical working-set
4403
- * 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.
4404
4743
  *
4405
- * llama.cpp at Q4_K_M with 8K ctx: ~1.20 × on-disk.
4406
- * 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.
4407
4749
  *
4408
4750
  * ds4 is special: it streams MoE experts from SSD, so its resident
4409
4751
  * working set is bounded by the configured expert-cache budget + KV +
@@ -4412,7 +4754,9 @@ declare class CapacityBroker {
4412
4754
  * conservative streaming working set so a 64GB box is never told an
4413
4755
  * 87GB DeepSeek-V4 model "can't fit" (which is the whole point of ds4).
4414
4756
  */
4415
- static estimateResidentBytes(engine: LocalProviderName, approxSizeBytes: number): number;
4757
+ static estimateResidentBytes(engine: LocalProviderName, approxSizeBytes: number, opts?: {
4758
+ mmprojBytes?: number;
4759
+ }): number;
4416
4760
  }
4417
4761
 
4418
4762
  /**
@@ -4581,6 +4925,9 @@ interface PoolSnapshot {
4581
4925
  */
4582
4926
  interface PooledQueueSummary {
4583
4927
  running: number;
4928
+ /** See {@link ProviderQueue.describe}. Summed across replicas like `running`. */
4929
+ runningInteractive: number;
4930
+ runningBackground: number;
4584
4931
  queuedInteractive: number;
4585
4932
  queuedBackground: number;
4586
4933
  /** Pending ambient entries held by the admission gate across replicas. */
@@ -4663,6 +5010,18 @@ declare class ProviderPool {
4663
5010
  * Returns false when the engine has already gone away.
4664
5011
  */
4665
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[]>;
4666
5025
  /**
4667
5026
  * Evict a specific entry. Awaits the provider's `shutdown` so the
4668
5027
  * disk-cache flush (Tier 1 prior plan) completes before the broker
@@ -5191,6 +5550,17 @@ declare class ContentIndex {
5191
5550
  stale: number;
5192
5551
  pending: number;
5193
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>;
5194
5564
  /** One file's boekwachter review (`file_review` tool). */
5195
5565
  fileReview(projectId: string, relPath: string): Promise<FileReviewResponse>;
5196
5566
  /** Flat issue query across current-hash reviews (`list_file_issues` tool). */
@@ -5205,6 +5575,28 @@ declare class ContentIndex {
5205
5575
  settleBoekwachterIssuesForTask(projectId: string, taskRef: string, outcome: 'complete' | 'canceled'): Promise<number>;
5206
5576
  private boekwachterIssuesForPath;
5207
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>>;
5208
5600
  readDocAsMarkdown(projectId: string, relPath: string): Promise<ReadDocAsMarkdownResponse>;
5209
5601
  /**
5210
5602
  * Schedule a rebuild of the project's artifacts-corpus index. Debounced per
@@ -5787,6 +6179,19 @@ interface GateCheckOutcome {
5787
6179
  detail: string;
5788
6180
  /** Machine evidence preserved from the rich CheckResults (arrays sliced to ≤10). */
5789
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;
5790
6195
  }
5791
6196
  interface GateCheckResult {
5792
6197
  pass: boolean;
@@ -5874,11 +6279,17 @@ declare function gateCheckLabel(c: GateCheck): string;
5874
6279
  * stage 3 — stop: pause with the full attempt trail as diagnosis.
5875
6280
  *
5876
6281
  * The plateau signature hashes failing-check IDENTITY (GateCheckOutcome
5877
- * labels), not prose and not content bytes: byte churn with an unmoved
5878
- * failure set IS the plateau (the eval harness's retryLoopSniffKey
5879
- * lesson), while clearing any single check changes the signature and
5880
- * resets the ladder. Kill-switch: GEZEL_DISABLE_GATE_ESCALATION=1
5881
- * 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.
5882
6293
  */
5883
6294
 
5884
6295
  type EscalationStage = 0 | 1 | 2 | 3;
@@ -6436,14 +6847,18 @@ declare class TaskManager {
6436
6847
  */
6437
6848
  private runCompletionGate;
6438
6849
  /**
6439
- * A rejected gate is unsatisfiable when a FAILING check reads the
6440
- * workspace tree while gezel workspace writes are off for the project
6441
- * (gate scripts always read the workspace, so a script rejection
6442
- * qualifies too). Returns the workspace files the failing checks name
6443
- * (possibly empty), or null when the gate is repairable. Drawer-only
6444
- * failures stay repairable — the artifacts drawer is deliberately
6445
- * 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.
6446
6860
  */
6861
+ allActiveStepsBlockedByWorkspaceWrites(projectId: string, tasks: Task[]): Promise<boolean>;
6447
6862
  /**
6448
6863
  * The workspace files a step must produce that no assignee is able to
6449
6864
  * write. The proactive twin of {@link unsatisfiableWorkspaceGateFiles}:
@@ -6464,6 +6879,22 @@ declare class TaskManager {
6464
6879
  * must skip the handoff — dispatching a gezel here cannot succeed.
6465
6880
  */
6466
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
+ */
6467
6898
  private unsatisfiableWorkspaceGateFiles;
6468
6899
  /**
6469
6900
  * Gate-driven loop-back: re-activate `targetId` WITHOUT completing the
@@ -6471,6 +6902,11 @@ declare class TaskManager {
6471
6902
  * hook even when the target IS the gated step itself — `onReject:
6472
6903
  * <self>` deliberately produces a fresh activation + handoff session
6473
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}.
6474
6910
  */
6475
6911
  private reactivateStepForGate;
6476
6912
  /**
@@ -6847,6 +7283,12 @@ declare class EngineRouter {
6847
7283
  * Provider is fixed to the caller's chosen engine.
6848
7284
  */
6849
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[]>;
6850
7292
  /** Unload one resident replica immediately, provided it is still idle. */
6851
7293
  unloadIdle(provider: LocalProviderName, modelId: string, replicaIdx: number): Promise<boolean>;
6852
7294
  snapshot(): PoolSnapshot;
@@ -7609,6 +8051,35 @@ interface ModelBundleSource {
7609
8051
  catalogVersion?: string;
7610
8052
  }
7611
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
+
7612
8083
  interface IncompleteModelDownload {
7613
8084
  id: string;
7614
8085
  /** Total bytes on disk across the directory (payload + `.partial`). */
@@ -7711,6 +8182,19 @@ interface InstalledMlxModel {
7711
8182
  * Delete that can only fail.
7712
8183
  */
7713
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;
7714
8198
  }
7715
8199
  /**
7716
8200
  * Snapshot of an in-flight MLX install — same shape as
@@ -7830,6 +8314,19 @@ declare class MlxModelManager {
7830
8314
  constructor(opts: MlxModelManagerOptions);
7831
8315
  getActiveInstalls(): MlxActiveInstallSnapshot[];
7832
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;
7833
8330
  /**
7834
8331
  * List incomplete MLX downloads — directories that hold bytes but no
7835
8332
  * `manifest.json`, so they never surface in {@link listInstalled}. Tagged
@@ -7877,6 +8374,14 @@ declare class MlxModelManager {
7877
8374
  * worker owns the error.
7878
8375
  */
7879
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;
7880
8385
  /** Warn once per model directory per process, then stay quiet. */
7881
8386
  private warnSkip;
7882
8387
  private loadInstalled;
@@ -8058,6 +8563,15 @@ interface InstalledLlamaCppModel {
8058
8563
  * video inputs on llama-server. Absent → text-only model.
8059
8564
  */
8060
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;
8061
8575
  /**
8062
8576
  * Absolute path to a speculative-decoding companion GGUF, when the
8063
8577
  * catalog ships one. The launcher forwards it as
@@ -8096,15 +8610,20 @@ interface InstalledLlamaCppModel {
8096
8610
  */
8097
8611
  readOnly?: boolean;
8098
8612
  /**
8099
- * True when the catalog now ships a different version than the one this
8100
- * model was downloaded against — a newer build is available. Lets the
8101
- * model manager offer "Update" (re-download + replace in place) instead
8102
- * 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
8103
8620
  * {@link LlamaCppModelManager.listInstalled} against the live catalog.
8104
8621
  */
8105
8622
  updateAvailable?: boolean;
8106
8623
  /** The catalog's current version, when it differs from the installed one. */
8107
8624
  availableVersion?: string;
8625
+ /** What changed, in one sentence, for the model manager's tooltip. */
8626
+ updateReason?: string;
8108
8627
  }
8109
8628
  type InstallEvent = {
8110
8629
  type: 'progress';
@@ -8300,6 +8819,19 @@ declare class LlamaCppModelManager {
8300
8819
  * needs the `contextWindow` to set `--ctx-size` correctly.
8301
8820
  */
8302
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;
8303
8835
  delete(id: string): Promise<void>;
8304
8836
  /** Provider-owned snapshot used by the streaming `.gezmodel` exporter. */
8305
8837
  getModelBundleSource(id: string): Promise<ModelBundleSource>;
@@ -8335,6 +8867,19 @@ declare class LlamaCppModelManager {
8335
8867
  * cleanup with each early `return` and silently rot.
8336
8868
  */
8337
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;
8338
8883
  /** Warn once per model directory per process, then stay quiet. */
8339
8884
  private warnSkip;
8340
8885
  private loadInstalled;
@@ -8436,6 +8981,55 @@ interface RemotesRegistry {
8436
8981
  touch(remoteId: string): void;
8437
8982
  }
8438
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
+
8439
9033
  /**
8440
9034
  * In-memory per-session progress telemetry. Counts the live signals the
8441
9035
  * chat layer already observes — streamed deltas, wire pulses, heartbeats,
@@ -8523,6 +9117,17 @@ interface TaskBudgetSnapshot {
8523
9117
  inputTokens: number;
8524
9118
  }
8525
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
+
8526
9131
  type TurnMessageOrigin = 'direct-user' | 'question-answer' | 'cross-gezel' | 'background-nudge' | 'system';
8527
9132
  interface GateScriptDiagnostic {
8528
9133
  scriptName: string;
@@ -8994,6 +9599,7 @@ declare class ChatManager {
8994
9599
  private readonly activeMemoryExtractions;
8995
9600
  private readonly store;
8996
9601
  private readonly events;
9602
+ private readonly externalConversations;
8997
9603
  private readonly memory;
8998
9604
  private readonly historyManager?;
8999
9605
  private readonly catalog;
@@ -9065,6 +9671,22 @@ declare class ChatManager {
9065
9671
  */
9066
9672
  setContentIndex(index: ContentIndex): void;
9067
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;
9068
9690
  /**
9069
9691
  * Wire the Keurmeester supervision engine. Set by `service.ts` after
9070
9692
  * both exist (the KeurmeesterManager needs `oneShotCompletion`, so
@@ -9112,6 +9734,39 @@ declare class ChatManager {
9112
9734
  */
9113
9735
  private resolveEffectiveContextWindow;
9114
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;
9115
9770
  /**
9116
9771
  * List the chat models a paired server offers, namespaced for A
9117
9772
  * (`remote:<remoteId>/<model>`). Used by the model picker. Returns [] if the
@@ -9206,10 +9861,19 @@ declare class ChatManager {
9206
9861
  projectId: string;
9207
9862
  createdAt: string;
9208
9863
  title: string;
9209
- 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";
9210
9865
  lastActivityAt: string;
9211
9866
  taskRef?: string | undefined;
9212
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;
9213
9877
  model?: string | undefined;
9214
9878
  archived?: boolean | undefined;
9215
9879
  lastTurnError?: string | undefined;
@@ -9461,6 +10125,13 @@ declare class ChatManager {
9461
10125
  */
9462
10126
  interruptWithMessage(sessionId: string, userText: string): Promise<ChatMessage>;
9463
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>;
9464
10135
  /**
9465
10136
  * The set of tool names a session's active craftbook pre-authorizes
9466
10137
  * (its `autoAllow` toolsets' tools). The Claude-CLI permission broker
@@ -9489,11 +10160,10 @@ declare class ChatManager {
9489
10160
  * effort). Surfaced via `GET /api/sessions/:id/debug` and consumed
9490
10161
  * by the UI's debug-mode "copy debug bundle" button.
9491
10162
  *
9492
- * This re-runs `buildSessionOpts` rather than reading whatever was
9493
- * captured at session-creation time the *current* prompt is what
9494
- * matters when an engineer is investigating "why did the model do
9495
- * X right now". Recent edits to the gezel's about, project context,
9496
- * 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.
9497
10167
  *
9498
10168
  * `atMessageTimestamp`: when set (ISO string from `ChatMessage.at`),
9499
10169
  * slice messages whose `at <= atMessageTimestamp` so the bundle
@@ -9512,8 +10182,26 @@ declare class ChatManager {
9512
10182
  * test teardown (or a graceful process exit) races them.
9513
10183
  */
9514
10184
  trackBackground(promise: Promise<unknown>): void;
9515
- /** Await every currently-tracked background promise, then return. */
9516
- 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>;
9517
10205
  createSession(args: {
9518
10206
  gezelId: string;
9519
10207
  projectId?: string;
@@ -10362,9 +11050,22 @@ declare class ChatManager {
10362
11050
  * Tear down cached sessions, tool bridges, and provider engines so the
10363
11051
  * next session picks up new config.
10364
11052
  *
10365
- * Default (hard) mode tears down everything immediately correct for
10366
- * credential / provider / endpoint changes, where the old engine state
10367
- * 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.
10368
11069
  *
10369
11070
  * `deferBusy` mode is for pure model-preference changes (default chat
10370
11071
  * model, reasoning effort), where the only thing that needs to flow
@@ -10382,6 +11083,8 @@ declare class ChatManager {
10382
11083
  resetClient(opts?: {
10383
11084
  deferBusy?: boolean;
10384
11085
  restoreSeededProviders?: boolean;
11086
+ /** See method doc. Defaults to `'release-idle'` — never severs a live turn. */
11087
+ engines?: 'release-idle' | 'force';
10385
11088
  }): Promise<void>;
10386
11089
  /**
10387
11090
  * Stop admitting work and actively unwind every live turn while the HTTP
@@ -10481,6 +11184,8 @@ declare class ChatManager {
10481
11184
  * expansion) — those must keep today's plain-background behavior.
10482
11185
  */
10483
11186
  ambient?: boolean;
11187
+ /** Override the background default for explicitly user-awaited utility work. */
11188
+ lane?: Lane;
10484
11189
  /**
10485
11190
  * Live streaming hooks for callers that surface progress (the
10486
11191
  * transform dialog's metacommentary feed). `onDelta` is the
@@ -10568,6 +11273,12 @@ declare class ChatManager {
10568
11273
  * immediately). No-op when the session isn't mid-turn.
10569
11274
  */
10570
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;
10571
11282
  /**
10572
11283
  * Whether the post-turn memory extraction should fire now. The
10573
11284
  * cadence depends on how heavyweight the provider's one-shot path
@@ -10666,6 +11377,20 @@ declare class ChatManager {
10666
11377
  * a test seam) and is deliberately left alone.
10667
11378
  */
10668
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;
10669
11394
  /**
10670
11395
  * Resolve (or build) the router. Returns `null` only when no local
10671
11396
  * provider is wired up at all (cloud-only installs); production
@@ -10892,8 +11617,17 @@ declare class ChatManager {
10892
11617
  * pool replica. This is the broker-side half of remote admission: focusing a
10893
11618
  * chat needs the live memory clamp so the user daemon can size its prompt,
10894
11619
  * but it must not load the model or evict somebody else's resident engine.
10895
- */
10896
- 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>;
10897
11631
  /**
10898
11632
  * Reservations held by models OTHER than the one being previewed.
10899
11633
  *
@@ -11025,6 +11759,20 @@ declare class ChatManager {
11025
11759
  * could do — so the launch config, not the catalog tag, is the source.
11026
11760
  */
11027
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;
11028
11776
  private resolveUserPromptPrelude;
11029
11777
  /**
11030
11778
  * Get or build the live state for a session. On first use after process
@@ -12073,6 +12821,23 @@ declare class GitManager {
12073
12821
  private withLock;
12074
12822
  }
12075
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
+
12076
12841
  /**
12077
12842
  * TaskRunner
12078
12843
  *
@@ -12157,8 +12922,12 @@ interface PendingHandoff {
12157
12922
  * activated during the day is filed as scheduled work immediately,
12158
12923
  * rather than counting as a backlog item for the up-to-one-tick window
12159
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.
12160
12929
  */
12161
- heldFor?: 'night-shift' | 'provider-busy';
12930
+ heldFor?: 'night-shift' | 'night-quota' | 'provider-busy';
12162
12931
  }
12163
12932
  /** One side of the pending split — see {@link TaskRunner.snapshot}. */
12164
12933
  interface TaskHandoffBucket {
@@ -12262,6 +13031,13 @@ interface TaskRunnerOptions {
12262
13031
  * the engine. Defaults to never-active.
12263
13032
  */
12264
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>;
12265
13041
  }
12266
13042
  declare class TaskRunner {
12267
13043
  private readonly store;
@@ -12271,6 +13047,7 @@ declare class TaskRunner {
12271
13047
  private readonly isNightShiftActive;
12272
13048
  private readonly isNightShiftPending;
12273
13049
  private readonly isIndexCatchUpActive;
13050
+ private readonly nightQuotaHold;
12274
13051
  private readonly pending;
12275
13052
  private readonly activeDispatches;
12276
13053
  private nextId;
@@ -12403,6 +13180,14 @@ declare class WorkspaceIndexManager {
12403
13180
  /** Per-project promise chain so two concurrent scans on the same
12404
13181
  * project serialize rather than racing on the disk. */
12405
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;
12406
13191
  private startupTimer;
12407
13192
  private tickTimer;
12408
13193
  constructor(opts: WorkspaceIndexManagerOptions);
@@ -12440,7 +13225,7 @@ declare class WorkspaceIndexManager {
12440
13225
  * autocomplete. The index stores files only, so directories are derived:
12441
13226
  * each match collapses to its next path segment after the prefix — a
12442
13227
  * directory (trailing `/`, so the user can descend) or the full file path.
12443
- * 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).
12444
13229
  * Case-insensitive prefix; deduped; capped.
12445
13230
  */
12446
13231
  searchWorkspaceFiles(projectId: string, prefix: string, limit?: number): Promise<string[]>;
@@ -12486,6 +13271,7 @@ declare class WorkspaceIndexManager {
12486
13271
  private readMetaFromDisk;
12487
13272
  private isStale;
12488
13273
  private mruIndex;
13274
+ private isSharedLibrary;
12489
13275
  /**
12490
13276
  * Missing preserves the historical indexed-by-default behavior. Reads the
12491
13277
  * live project each time so toggling the setting takes effect without a
@@ -13085,6 +13871,7 @@ declare class IndexEnrichmentManager {
13085
13871
  * awaited) and then wakes the TaskRunner still gets the dispatch hold.
13086
13872
  */
13087
13873
  catchUpAll(): Promise<void>;
13874
+ private isSharedLibrary;
13088
13875
  private runDrive;
13089
13876
  /** Exposed for tests: run one batch ignoring the timers (still idle-gated). */
13090
13877
  tick(): Promise<void>;
@@ -13104,17 +13891,11 @@ interface SessionSearchHit {
13104
13891
  lastActivityAt: string;
13105
13892
  archived: boolean;
13106
13893
  }
13107
- interface DocumentSearchHit {
13108
- path: string;
13109
- lineStart: number;
13110
- snippet: string;
13111
- }
13112
13894
  interface GlobalIndexStatus {
13113
13895
  available: boolean;
13114
13896
  sessions: number;
13115
13897
  history: number;
13116
13898
  historyBackfilledAt: string | null;
13117
- documents: number;
13118
13899
  }
13119
13900
  declare class GlobalIndex {
13120
13901
  private readonly home;
@@ -13130,7 +13911,6 @@ declare class GlobalIndex {
13130
13911
  * falls back to the JSONL scan.
13131
13912
  */
13132
13913
  searchHistory(filter?: HistoryFilter): Promise<HistoryEvent[] | null>;
13133
- searchDocuments(q: string, maxResults?: number): Promise<DocumentSearchHit[]>;
13134
13914
  status(): Promise<GlobalIndexStatus>;
13135
13915
  }
13136
13916
 
@@ -13151,44 +13931,6 @@ declare class IndexingJobControl {
13151
13931
  private noteAuthor;
13152
13932
  }
13153
13933
 
13154
- interface ActivityTrackerOptions {
13155
- store: Store;
13156
- history: HistoryManager;
13157
- chatEvents: ChatEventBus;
13158
- now?: () => number;
13159
- persistThresholdMs?: number;
13160
- }
13161
- declare class ActivityTracker {
13162
- private readonly store;
13163
- private readonly history;
13164
- private readonly chatEvents;
13165
- private readonly now;
13166
- private readonly persistThresholdMs;
13167
- /** Newest observed stamp per project (ms epoch). */
13168
- private readonly latest;
13169
- /** Stamp last written to (or read from) disk per project. */
13170
- private readonly persisted;
13171
- /** Projects whose on-disk stamp has been loaded into `persisted`. */
13172
- private readonly loads;
13173
- private readonly writing;
13174
- private unsubs;
13175
- constructor(opts: ActivityTrackerOptions);
13176
- start(): void;
13177
- stop(): Promise<void>;
13178
- stamp(projectId: string, atMs: number): void;
13179
- /**
13180
- * Newest known activity for a project — max of the in-memory stamp
13181
- * and the on-disk one (loaded once). Null when the project has never
13182
- * been observed.
13183
- */
13184
- lastActivityAt(projectId: string): Promise<string | null>;
13185
- /** Persist every stamp that is newer than its on-disk value. */
13186
- flush(): Promise<void>;
13187
- private maybePersist;
13188
- private write;
13189
- private ensureLoaded;
13190
- }
13191
-
13192
13934
  type StatusOneShot = (prompt: string, timeoutMs: number, opts: {
13193
13935
  gezelId: string;
13194
13936
  jobLabel: string;
@@ -13439,6 +14181,26 @@ interface ChatModelInstallRegistries {
13439
14181
  mlx: MlxChatInstallRegistry;
13440
14182
  }
13441
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
+
13442
14204
  /**
13443
14205
  * AudioProvider — service-side abstractions for speech-to-text (STT)
13444
14206
  * and text-to-speech (TTS).
@@ -14145,6 +14907,11 @@ interface InstalledVideoModelInfo {
14145
14907
  name: string;
14146
14908
  approxSizeBytes: number;
14147
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;
14148
14915
  }
14149
14916
  /**
14150
14917
  * Multi-file pull progress. Mirrors `MlxInstallEvent` (video models are
@@ -14592,6 +15359,36 @@ declare class SearchService {
14592
15359
  private merge;
14593
15360
  }
14594
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
+
14595
15392
  /**
14596
15393
  * The pinned system-toolset list.
14597
15394
  *
@@ -14880,6 +15677,24 @@ interface NightShiftManagerOptions {
14880
15677
  intervalMs?: number;
14881
15678
  /** Clock override for tests. */
14882
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[];
14883
15698
  }
14884
15699
  /**
14885
15700
  * Owns the Night Shift ON/OFF state. Lifecycle mirrors `TaskScheduler` /
@@ -14893,6 +15708,12 @@ interface NightShiftManagerOptions {
14893
15708
  * pending night-shift tasks remain, then reverts to scheduled logic.
14894
15709
  * - SCHEDULED: outside the window → off (latch cleared); window open but
14895
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.
14896
15717
  *
14897
15718
  * Consumers read `isActive()` synchronously: `TaskRunner` (dispatch gating
14898
15719
  * + priority), `TaskScheduler` (cron-spawn gating), `IndexEnrichmentManager`
@@ -14904,10 +15725,14 @@ declare class NightShiftManager {
14904
15725
  private readonly events;
14905
15726
  private readonly intervalMs;
14906
15727
  private readonly now;
15728
+ private readonly quotaGate?;
15729
+ private readonly resolveProviderName?;
14907
15730
  private timer;
14908
15731
  private ticking;
14909
15732
  private active;
14910
15733
  private src;
15734
+ /** When the running shift began; null while nothing is running. */
15735
+ private startedAt;
14911
15736
  /** Window key we've drained-and-latched off for; cleared next window. */
14912
15737
  private latchedOffForWindowKey;
14913
15738
  /** Whether a manual ("go to lunch") shift has been requested. */
@@ -14920,11 +15745,44 @@ declare class NightShiftManager {
14920
15745
  private enabled;
14921
15746
  /** Window config from the last tick — drives the synchronous day-key. */
14922
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;
14923
15752
  constructor(opts: NightShiftManagerOptions);
14924
15753
  start(): void;
14925
15754
  stop(): void;
14926
15755
  isActive(): boolean;
14927
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>;
14928
15786
  /**
14929
15787
  * Whether the configured nightly window is open right now — used by the
14930
15788
  * scheduler to confine a night-shift cron host's spawning to the window
@@ -14995,8 +15853,15 @@ declare class NightShiftManager {
14995
15853
  * exposes the same set for the UI's "what's the shift doing?" panel.
14996
15854
  */
14997
15855
  listPendingTasks(): Promise<Task[]>;
14998
- /** True if any active night-shift task still has work to do `todayKey`. */
14999
- 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;
15000
15865
  /** Active night-shift tasks pending `todayKey`, in `manager.list` order. */
15001
15866
  private pendingNightShiftTasks;
15002
15867
  /** Single transition chokepoint: diff, broadcast, log. */
@@ -15509,6 +16374,14 @@ declare class TerminalManager {
15509
16374
  private collectListingFileReferences;
15510
16375
  }
15511
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
+
15512
16385
  interface OllamaEmulationStatus {
15513
16386
  listening: boolean;
15514
16387
  port?: number;
@@ -15550,6 +16423,12 @@ interface ServiceContext {
15550
16423
  * manual run.
15551
16424
  */
15552
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;
15553
16432
  scriptRunner: ScriptRunner;
15554
16433
  catalog: CatalogService;
15555
16434
  /**
@@ -15713,6 +16592,12 @@ interface ServiceContext {
15713
16592
  ollamaEmulation: OllamaEmulationController;
15714
16593
  /** Gezel-owned Codex profile, credential, and loopback bridge lifecycle. */
15715
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;
15716
16601
  /**
15717
16602
  * Hex SHA-256 of the daemon's current TLS cert DER, when serving HTTPS.
15718
16603
  * `/v1/identity` signs this with the device identity key so a paired client
@@ -15743,6 +16628,13 @@ interface ServiceContext {
15743
16628
  * (the worker writes a sentinel file so the next boot can detect a
15744
16629
  * crashed mid-move). */
15745
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;
15746
16638
  /** Background workspace indexer: commands + files + token index. */
15747
16639
  workspaceIndex: WorkspaceIndexManager;
15748
16640
  /** Content index (code/doc intelligence) backing the code-intel MCP tools. */
@@ -15847,6 +16739,16 @@ interface StartServiceOptions {
15847
16739
  codexHome?: string;
15848
16740
  /** Exact Codex bridge port override (`0` = ephemeral); production derives one from `home`. */
15849
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;
15850
16752
  }
15851
16753
  interface RunningService {
15852
16754
  context: ServiceContext;