@pellux/goodvibes-tui 1.19.6 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +143 -1613
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -7
- package/src/cli/ensure-goodvibes-gitignore.ts +16 -4
- package/src/cli/entrypoint.ts +5 -1
- package/src/cli/launch-auto-update.ts +63 -4
- package/src/cli/tui-startup.ts +131 -15
- package/src/config/surface.ts +21 -0
- package/src/config/tui-extension-settings.ts +9 -1
- package/src/core/conversation-line-cache.ts +1 -1
- package/src/core/conversation-rendering.ts +70 -24
- package/src/core/conversation-tool-groups.ts +23 -10
- package/src/core/conversation-types.ts +9 -0
- package/src/core/rewind-turn-anchors.ts +12 -13
- package/src/core/session-recovery.ts +59 -2
- package/src/core/session-resume-core.ts +206 -0
- package/src/core/transcript-journal.ts +32 -6
- package/src/core/turn-event-wiring.ts +55 -12
- package/src/input/command-args-hint.ts +11 -2
- package/src/input/command-registry.ts +19 -0
- package/src/input/commands/checkpoint-runtime.ts +3 -3
- package/src/input/commands/health-runtime.ts +1 -3
- package/src/input/commands/local-runtime.ts +1 -1
- package/src/input/commands/rewind-runtime.ts +132 -1
- package/src/input/commands/runtime-services.ts +11 -1
- package/src/input/commands/secret-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +9 -1
- package/src/input/commands/session-picker-filter.ts +28 -0
- package/src/input/commands/session-workflow.ts +62 -73
- package/src/input/commands/session.ts +36 -1
- package/src/input/commands/shell-core.ts +57 -5
- package/src/input/commands/update-runtime.ts +120 -4
- package/src/input/commands/websearch-runtime.ts +1 -1
- package/src/input/handler-content-actions.ts +63 -39
- package/src/input/handler-feed-routes.ts +17 -3
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +19 -23
- package/src/input/handler-modal-token-routes.ts +5 -0
- package/src/input/handler-types.ts +4 -1
- package/src/input/handler-ui-state.ts +6 -2
- package/src/input/handler.ts +19 -9
- package/src/input/search.ts +94 -2
- package/src/input/selection-modal-queue.ts +176 -0
- package/src/main.ts +48 -44
- package/src/panels/builtin/operations.ts +2 -1
- package/src/panels/provider-health-domains.ts +2 -2
- package/src/renderer/block-actions-overlay.ts +59 -0
- package/src/renderer/block-actions.ts +1 -3
- package/src/renderer/context-status-hint.ts +73 -5
- package/src/renderer/conversation-layout.ts +16 -0
- package/src/renderer/conversation-overlays.ts +6 -0
- package/src/renderer/help-overlay.ts +69 -53
- package/src/renderer/selection-modal-overlay.ts +66 -10
- package/src/renderer/shell-surface.ts +15 -0
- package/src/renderer/tool-result-expanded-lines.ts +48 -0
- package/src/renderer/ui-factory.ts +1 -1
- package/src/runtime/bootstrap-command-context.ts +5 -1
- package/src/runtime/bootstrap-command-parts.ts +5 -2
- package/src/runtime/bootstrap-core.ts +3 -3
- package/src/runtime/bootstrap-hook-bridge.ts +62 -44
- package/src/runtime/bootstrap-shell.ts +31 -16
- package/src/runtime/bootstrap.ts +13 -18
- package/src/runtime/code-index-services.ts +19 -0
- package/src/runtime/durability-services.ts +11 -7
- package/src/runtime/index.ts +12 -0
- package/src/runtime/process-lifecycle.ts +63 -2
- package/src/runtime/recovery-autosave.ts +48 -0
- package/src/runtime/recovery-offer-wiring.ts +89 -0
- package/src/runtime/recovery-prompt.ts +312 -0
- package/src/runtime/resume-notice.ts +28 -93
- package/src/runtime/services.ts +14 -16
- package/src/runtime/session-liveness-marker.ts +120 -0
- package/src/runtime/session-pointer-surface.ts +22 -0
- package/src/runtime/session-resume-liveness-confirm.ts +84 -0
- package/src/runtime/session-storage-services.ts +41 -0
- package/src/runtime/workspace-checkpointing.ts +10 -2
- package/src/shell/recovery-input-helpers.ts +7 -3
- package/src/shell/retry-affordance.ts +123 -0
- package/src/shell/ui-openers.ts +9 -0
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recovery-prompt.ts — the startup "a recovery snapshot exists, do you want
|
|
3
|
+
* it?" flow.
|
|
4
|
+
*
|
|
5
|
+
* A crash-recovery snapshot used to get one sentence in the boot resume
|
|
6
|
+
* notice, which left the operator to read it and retype a command — and for a
|
|
7
|
+
* session that crashed before its first clean save, there was no command that
|
|
8
|
+
* reached the snapshot at all, so the sentence was a dead end. This module
|
|
9
|
+
* replaces that with an explicit two-step ask:
|
|
10
|
+
*
|
|
11
|
+
* 1. "Resume it?" [Resume] [Not now]
|
|
12
|
+
* 2. on decline: "Remove this recovery point?" [Keep] [Remove]
|
|
13
|
+
*
|
|
14
|
+
* Rules this flow keeps:
|
|
15
|
+
* - Nothing is ever applied without the user picking Resume. There is no
|
|
16
|
+
* silent auto-restore path here, and this module never loads a snapshot
|
|
17
|
+
* just to look at it — `checkRecoveryFile` reports, `consumeRecovery`
|
|
18
|
+
* loads, and only the second one runs after an explicit yes.
|
|
19
|
+
* - The SDK's `consumeRecovery` is load-then-delete: the snapshot file is
|
|
20
|
+
* retired only after a successful load, so a failed read can never
|
|
21
|
+
* destroy state that was never actually recovered.
|
|
22
|
+
* - Every fact shown is one we actually have (session id, snapshot age,
|
|
23
|
+
* title when the snapshot carries one, byte size when the file is where
|
|
24
|
+
* we can stat it). Nothing is estimated to fill the sentence out.
|
|
25
|
+
* - Those facts live in a row's `detail`, never in the modal title. The
|
|
26
|
+
* selection overlay truncates its title to the box width
|
|
27
|
+
* (selection-modal-overlay.ts) but wraps `detail` onto as many lines as it
|
|
28
|
+
* needs with no cap, so detail is the only place long copy is guaranteed
|
|
29
|
+
* to be shown in full. Titles here stay short enough to survive a narrow
|
|
30
|
+
* terminal intact.
|
|
31
|
+
* - A snapshot whose session still has a live pid marker is not offered at
|
|
32
|
+
* all: another terminal is refreshing it right now, so it is not an
|
|
33
|
+
* orphaned crash and resuming it here would fork that instance's live
|
|
34
|
+
* state.
|
|
35
|
+
* - Escape is not an answer. Dismissing either modal leaves the snapshot
|
|
36
|
+
* exactly where it is; only "Remove" deletes anything.
|
|
37
|
+
* - The destructive row is never the one Enter is already sitting on. Both
|
|
38
|
+
* modals open on their harmless first row, so answering either of them
|
|
39
|
+
* without reading keeps the snapshot.
|
|
40
|
+
* - Keep (or a dismissal) stays quiet for the rest of the run. The snapshot
|
|
41
|
+
* is offered again on the next launch, not again in this one.
|
|
42
|
+
*/
|
|
43
|
+
import { statSync } from 'node:fs';
|
|
44
|
+
import { checkRecoveryFile, checkRecoveryForSession, consumeRecovery, removeRecoveryPoint } from '@/runtime/index.ts';
|
|
45
|
+
import type { RecoveryFileInfo, SessionSurface } from '@/runtime/index.ts';
|
|
46
|
+
import { checkSessionLiveness } from './session-liveness-marker.ts';
|
|
47
|
+
import type { SelectionItem, SelectionResult } from '../input/selection-modal.ts';
|
|
48
|
+
|
|
49
|
+
/** How the offer ended. Returned so startup wiring (and tests) can assert the real outcome. */
|
|
50
|
+
export type RecoveryPromptOutcome =
|
|
51
|
+
| 'none' // no snapshot, or one that belongs to a still-live session
|
|
52
|
+
| 'resumed' // user chose Resume and the snapshot was applied
|
|
53
|
+
| 'resume-failed' // user chose Resume but the snapshot could not be loaded
|
|
54
|
+
| 'removed' // user declined and chose Remove
|
|
55
|
+
| 'kept'; // user declined and chose Keep, or dismissed a modal
|
|
56
|
+
|
|
57
|
+
/** Open a selection modal and resolve with the chosen item id (null on dismissal). */
|
|
58
|
+
export type SelectionOpener = (
|
|
59
|
+
title: string,
|
|
60
|
+
items: SelectionItem[],
|
|
61
|
+
opts: { preSelectId?: string; allowSearch?: boolean; primaryVerbLabel?: string } | undefined,
|
|
62
|
+
callback: (result: SelectionResult | null) => void,
|
|
63
|
+
) => void;
|
|
64
|
+
|
|
65
|
+
export interface RecoveryPromptDeps {
|
|
66
|
+
readonly surface: SessionSurface;
|
|
67
|
+
/** The shell's selection-modal opener. Absent in headless hosts — the flow then does nothing at all. */
|
|
68
|
+
readonly openSelection: SelectionOpener | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Apply an explicitly-accepted snapshot to the live conversation and report
|
|
71
|
+
* how many messages ended up in it. Supplied by the startup wiring so this
|
|
72
|
+
* module never reaches into conversation state itself.
|
|
73
|
+
*/
|
|
74
|
+
readonly applySnapshot: (payload: { readonly snapshot: Record<string, unknown>; readonly sessionId: string }) => number;
|
|
75
|
+
/** One-line honest receipt into the transcript. */
|
|
76
|
+
readonly receipt: (line: string) => void;
|
|
77
|
+
readonly render: () => void;
|
|
78
|
+
/** Injectable for tests. Defaults to the real wall clock. */
|
|
79
|
+
readonly now?: () => number;
|
|
80
|
+
/** Injectable for tests. Defaults to statSync on the surface's own recovery file. */
|
|
81
|
+
readonly snapshotBytes?: (sessionId: string) => number | null;
|
|
82
|
+
/**
|
|
83
|
+
* When set, offer THIS session's snapshot specifically (via
|
|
84
|
+
* `checkRecoveryForSession`) instead of the newest snapshot across every
|
|
85
|
+
* session (`checkRecoveryFile`). Used by the `--continue` / bare `--resume`
|
|
86
|
+
* pre-resume check (cli/tui-startup.ts): resuming a named session straight
|
|
87
|
+
* from its durable store would silently drop the tail of messages held only
|
|
88
|
+
* in a snapshot newer than that store — the same rule `checkRecoveryFile`
|
|
89
|
+
* uses, just scoped to the one session being resumed instead of "whichever
|
|
90
|
+
* session has the newest snapshot".
|
|
91
|
+
*/
|
|
92
|
+
readonly targetSessionId?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Honest fact formatting ─────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/** Human-readable age of a snapshot. Coarse on purpose — the exact second is not a fact worth claiming. */
|
|
98
|
+
export function formatSnapshotAge(ageMs: number): string {
|
|
99
|
+
if (ageMs < 0) return 'just now';
|
|
100
|
+
const minutes = Math.floor(ageMs / 60_000);
|
|
101
|
+
if (minutes < 1) return 'just now';
|
|
102
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
103
|
+
const hours = Math.floor(minutes / 60);
|
|
104
|
+
if (hours < 24) return `${hours}h ago`;
|
|
105
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Compact byte size. Only ever called with a size we actually read off disk. */
|
|
109
|
+
export function formatSnapshotSize(bytes: number): string {
|
|
110
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
111
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
112
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Short, fixed modal titles. Kept well inside the narrowest box width so neither is ever truncated. */
|
|
116
|
+
export const RECOVERY_OFFER_TITLE = 'Recovery snapshot found';
|
|
117
|
+
export const RECOVERY_RETIRE_TITLE = 'Remove recovery point?';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The facts line shown in the modal. Only includes what is genuinely known:
|
|
121
|
+
* the session id and age are always available from `checkRecoveryFile`; the
|
|
122
|
+
* title appears only when the snapshot carries a non-empty one; the size
|
|
123
|
+
* appears only when the file could be stat'd.
|
|
124
|
+
*/
|
|
125
|
+
export function describeRecoverySnapshot(
|
|
126
|
+
info: RecoveryFileInfo,
|
|
127
|
+
facts: { readonly nowMs: number; readonly bytes: number | null },
|
|
128
|
+
): string {
|
|
129
|
+
const parts = [`session ${info.sessionId}`, formatSnapshotAge(facts.nowMs - info.timestamp)];
|
|
130
|
+
if (info.title && info.title.trim()) parts.push(`"${info.title.trim()}"`);
|
|
131
|
+
if (facts.bytes !== null) parts.push(formatSnapshotSize(facts.bytes));
|
|
132
|
+
return parts.join(' · ');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The offer modal's rows. The snapshot's facts ride on the Resume row's
|
|
137
|
+
* `detail` — both because that is where they are decision-relevant ("this is
|
|
138
|
+
* what you would be loading") and because `detail` is the only field the
|
|
139
|
+
* selection overlay wraps rather than truncates, so the full description
|
|
140
|
+
* survives any terminal width.
|
|
141
|
+
*/
|
|
142
|
+
export function buildRecoveryOfferItems(facts: string): SelectionItem[] {
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
id: 'resume',
|
|
146
|
+
label: 'Resume it',
|
|
147
|
+
detail: `Loads ${facts} into this session. The recovery point is deleted once it loads.`,
|
|
148
|
+
primaryAction: 'select',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'not-now',
|
|
152
|
+
label: 'Not now',
|
|
153
|
+
detail: 'Start fresh. You will be asked whether to keep or remove the recovery point.',
|
|
154
|
+
primaryAction: 'select',
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The follow-up modal's rows, shown only after an explicit decline. Restates
|
|
161
|
+
* which snapshot is at stake.
|
|
162
|
+
*
|
|
163
|
+
* Keep comes first, and so is what a blind Enter lands on. This modal is the
|
|
164
|
+
* one place in the flow where a keypress can destroy a conversation, and it
|
|
165
|
+
* appears unrequested at boot, so the row under the cursor has to be the one
|
|
166
|
+
* that costs nothing: pressing Enter without reading leaves the snapshot on
|
|
167
|
+
* disk to be offered again. Deleting it takes a deliberate move down first.
|
|
168
|
+
*/
|
|
169
|
+
export function buildRecoveryRetireItems(facts: string): SelectionItem[] {
|
|
170
|
+
return [
|
|
171
|
+
{
|
|
172
|
+
id: 'keep',
|
|
173
|
+
label: 'Keep it',
|
|
174
|
+
detail: 'Leave the recovery point on disk. It will be offered again the next time this workspace opens.',
|
|
175
|
+
primaryAction: 'select',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'remove',
|
|
179
|
+
label: 'Remove it',
|
|
180
|
+
detail: `Deletes the recovery point for ${facts}. The conversation it holds cannot be recovered afterwards.`,
|
|
181
|
+
primaryAction: 'select',
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Flow ───────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
function ask(open: SelectionOpener, title: string, items: SelectionItem[]): Promise<string | null> {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
open(title, items, { allowSearch: false, primaryVerbLabel: 'Choose' }, (result) => {
|
|
191
|
+
resolve(result?.item.id ?? null);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function defaultSnapshotBytes(surface: SessionSurface, sessionId: string): number | null {
|
|
197
|
+
try {
|
|
198
|
+
return statSync(surface.recoveryFile(sessionId)).size;
|
|
199
|
+
} catch {
|
|
200
|
+
// The snapshot may have come from the SDK's legacy dual-read location,
|
|
201
|
+
// which this path cannot address. Report no size rather than a wrong one.
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Snapshots the user has already answered about (Keep or dismissal) during
|
|
208
|
+
* this process's lifetime. Two offer paths exist in one boot — the targeted
|
|
209
|
+
* pre-resume check for `--continue`/`--resume` and the general startup offer —
|
|
210
|
+
* and both can find the same snapshot; an answer given to either binds both,
|
|
211
|
+
* so "stays quiet for the rest of the run" holds across the pair. Resume and
|
|
212
|
+
* Remove retire the file itself, so only the declined cases need remembering.
|
|
213
|
+
*/
|
|
214
|
+
const answeredThisRun = new Set<string>();
|
|
215
|
+
|
|
216
|
+
/** Test seam: a fresh process has no answered offers. */
|
|
217
|
+
export function resetAnsweredRecoveryOffersForTest(): void {
|
|
218
|
+
answeredThisRun.clear();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Run the startup recovery offer. Resolves once the user has answered (or
|
|
223
|
+
* once it is established that there is nothing to ask about). Never throws:
|
|
224
|
+
* a failure anywhere in here must not take a boot down.
|
|
225
|
+
*/
|
|
226
|
+
export async function offerRecoverySnapshot(deps: RecoveryPromptDeps): Promise<RecoveryPromptOutcome> {
|
|
227
|
+
try {
|
|
228
|
+
const info = deps.targetSessionId !== undefined
|
|
229
|
+
? checkRecoveryForSession(deps.surface, deps.targetSessionId)
|
|
230
|
+
: checkRecoveryFile({ surface: deps.surface });
|
|
231
|
+
if (!info) return 'none';
|
|
232
|
+
// Already answered about this snapshot earlier in this same run (the
|
|
233
|
+
// targeted --continue offer and the general startup offer can both find
|
|
234
|
+
// it). The earlier answer stands; don't ask twice.
|
|
235
|
+
if (answeredThisRun.has(info.sessionId)) return 'kept';
|
|
236
|
+
// Another terminal is actively refreshing this snapshot — it is that
|
|
237
|
+
// instance's live state, not an orphaned crash.
|
|
238
|
+
if (checkSessionLiveness(deps.surface, info.sessionId).live) return 'none';
|
|
239
|
+
const open = deps.openSelection;
|
|
240
|
+
if (!open) return 'none';
|
|
241
|
+
|
|
242
|
+
const nowMs = deps.now?.() ?? Date.now();
|
|
243
|
+
const bytes = deps.snapshotBytes ? deps.snapshotBytes(info.sessionId) : defaultSnapshotBytes(deps.surface, info.sessionId);
|
|
244
|
+
const facts = describeRecoverySnapshot(info, { nowMs, bytes });
|
|
245
|
+
|
|
246
|
+
const answer = await ask(open, RECOVERY_OFFER_TITLE, buildRecoveryOfferItems(facts));
|
|
247
|
+
|
|
248
|
+
if (answer === 'resume') {
|
|
249
|
+
const { snapshot, consumed } = consumeRecovery(deps.surface, info.sessionId);
|
|
250
|
+
if (!snapshot || !consumed) {
|
|
251
|
+
// consumeRecovery leaves the file alone when the load fails, so the
|
|
252
|
+
// snapshot is still there to try again next launch. Say so.
|
|
253
|
+
deps.receipt('Recovery snapshot could not be read — it was left on disk and will be offered again next launch.');
|
|
254
|
+
deps.render();
|
|
255
|
+
return 'resume-failed';
|
|
256
|
+
}
|
|
257
|
+
const messageCount = deps.applySnapshot({ snapshot: snapshot as unknown as Record<string, unknown>, sessionId: info.sessionId });
|
|
258
|
+
deps.receipt(`Recovery snapshot restored: ${messageCount} message(s) from session ${info.sessionId}. The recovery point has been retired.`);
|
|
259
|
+
deps.render();
|
|
260
|
+
return 'resumed';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (answer !== 'not-now') {
|
|
264
|
+
// Dismissed rather than answered. Nothing is deleted and nothing is
|
|
265
|
+
// asked again this run.
|
|
266
|
+
answeredThisRun.add(info.sessionId);
|
|
267
|
+
return 'kept';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const retire = await ask(open, RECOVERY_RETIRE_TITLE, buildRecoveryRetireItems(facts));
|
|
271
|
+
if (retire === 'remove') {
|
|
272
|
+
const { removed } = removeRecoveryPoint(deps.surface, info.sessionId);
|
|
273
|
+
deps.receipt(removed
|
|
274
|
+
? `Recovery point removed (session ${info.sessionId}).`
|
|
275
|
+
: `No recovery point was found to remove (session ${info.sessionId}).`);
|
|
276
|
+
deps.render();
|
|
277
|
+
return 'removed';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
answeredThisRun.add(info.sessionId);
|
|
281
|
+
deps.receipt(`Recovery point kept (session ${info.sessionId}) — it will be offered again next launch.`);
|
|
282
|
+
deps.render();
|
|
283
|
+
return 'kept';
|
|
284
|
+
} catch {
|
|
285
|
+
// Best-effort by construction: a recovery offer that fails must leave the
|
|
286
|
+
// snapshot untouched and the boot unharmed.
|
|
287
|
+
return 'none';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Startup wiring ─────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Raise the recovery offer once the shell is up.
|
|
295
|
+
*
|
|
296
|
+
* Deliberately fire-and-forget and deliberately AFTER the first render: the
|
|
297
|
+
* modal is drawn by the render loop, so asking before a frame exists would
|
|
298
|
+
* mean a question nobody can see, and awaiting it would hold the terminal
|
|
299
|
+
* blank while the user was expected to answer. Scheduled on a macrotask so
|
|
300
|
+
* the caller's own initial render has completed first.
|
|
301
|
+
*
|
|
302
|
+
* Every failure mode here ends with the snapshot untouched: `offerRecoverySnapshot`
|
|
303
|
+
* swallows its own errors, and this wrapper adds a catch for the scheduling
|
|
304
|
+
* boundary.
|
|
305
|
+
*/
|
|
306
|
+
export function scheduleRecoveryOffer(deps: RecoveryPromptDeps): void {
|
|
307
|
+
setTimeout(() => {
|
|
308
|
+
void offerRecoverySnapshot(deps).catch(() => {
|
|
309
|
+
// Best-effort: a failed offer must never take the shell down.
|
|
310
|
+
});
|
|
311
|
+
}, 0).unref?.();
|
|
312
|
+
}
|
|
@@ -10,13 +10,16 @@
|
|
|
10
10
|
* what real state exists and how to reach it.
|
|
11
11
|
*
|
|
12
12
|
* Honesty constraints (verified against the actual runtime, not assumed):
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `/session resume <id
|
|
19
|
-
*
|
|
13
|
+
* - `/resume` and `/sessions` both exist and both work: `/resume`
|
|
14
|
+
* (session.ts) opens a zero-typing picker over saved sessions when
|
|
15
|
+
* called bare, and resumes directly when given an id/name; `/sessions`
|
|
16
|
+
* (session-content.ts) forwards a `resume <id>` subcommand to the same
|
|
17
|
+
* `/session resume` path. All three spellings — `/resume <id>`,
|
|
18
|
+
* `/session resume <id>`, `/sessions resume <id>` — reach the identical
|
|
19
|
+
* resume routine. This notice leads with the zero-typing picker
|
|
20
|
+
* (`/resume`) and keeps the direct, exact-id form (`/session resume
|
|
21
|
+
* <id>`) as a secondary hint, for the case of already knowing which
|
|
22
|
+
* session to reach for.
|
|
20
23
|
* - `/checkpoints` works with zero arguments and behaves correctly at any
|
|
21
24
|
* checkpoint count (including zero) — advertised whenever the checkpoint
|
|
22
25
|
* manager is available in this session.
|
|
@@ -27,19 +30,15 @@
|
|
|
27
30
|
* - Every clause is independently gated on real data: a claim about
|
|
28
31
|
* checkpoints/chain history is only made when that data is known; "no
|
|
29
32
|
* chain history" means no chain clause is printed, not a fabricated one.
|
|
30
|
-
* - A live recovery snapshot
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* session never completed a full turn has no working command that
|
|
37
|
-
* reaches it yet — reported honestly without one, rather than
|
|
38
|
-
* advertising a command that would fail. This module never auto-applies
|
|
39
|
-
* the snapshot itself; restoring it is always the user's explicit call.
|
|
33
|
+
* - A live crash-recovery snapshot is NOT this notice's business any more.
|
|
34
|
+
* It used to get a clause here, which meant an operator's only route back
|
|
35
|
+
* to a crashed session was reading a sentence and retyping a command. It
|
|
36
|
+
* is now an explicit ask-then-retire modal raised right after first
|
|
37
|
+
* render — see runtime/recovery-prompt.ts. Keeping a passive clause here
|
|
38
|
+
* as well would announce the same snapshot twice.
|
|
40
39
|
*/
|
|
41
40
|
|
|
42
|
-
import {
|
|
41
|
+
import { readLastSessionPointer, type SessionSurface } from '@/runtime/index.ts';
|
|
43
42
|
import type { WrfcChain } from '@pellux/goodvibes-sdk/platform/agents';
|
|
44
43
|
import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
45
44
|
import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
@@ -100,19 +99,6 @@ export interface ResumeNoticeFacts {
|
|
|
100
99
|
readonly lastChainOutcome: ChainOutcome | null;
|
|
101
100
|
/** Whether /recall (memory) is wired up in this session. */
|
|
102
101
|
readonly memoryAvailable: boolean;
|
|
103
|
-
/**
|
|
104
|
-
* A live recovery snapshot on disk (newer than the last clean save), if
|
|
105
|
-
* any — the SDK's checkRecoveryFile, written periodically while a session
|
|
106
|
-
* runs and left behind by one that never reached a clean save (a crash is
|
|
107
|
-
* one cause; a second TUI still running in the same workspace is another —
|
|
108
|
-
* this module has no liveness check, so it never claims a crash happened,
|
|
109
|
-
* only that a snapshot exists). `resumable` is true only when that exact
|
|
110
|
-
* session id is also loadable through the session manager, i.e.
|
|
111
|
-
* `/session resume <id>` genuinely reaches it; false means the snapshot
|
|
112
|
-
* exists but nothing reaches it yet (e.g. a crash during its very first
|
|
113
|
-
* turn, before any full save). Null when there is no live snapshot at all.
|
|
114
|
-
*/
|
|
115
|
-
readonly recoverySnapshot: { readonly sessionId: string; readonly resumable: boolean } | null;
|
|
116
102
|
}
|
|
117
103
|
|
|
118
104
|
function plural(n: number, word: string): string {
|
|
@@ -130,10 +116,8 @@ export function buildResumeNotice(facts: ResumeNoticeFacts): string | null {
|
|
|
130
116
|
const checkpointCount = facts.checkpointCount ?? 0;
|
|
131
117
|
const hasCheckpoints = checkpointsKnown && checkpointCount > 0;
|
|
132
118
|
const hasChainHistory = facts.lastChainOutcome !== null;
|
|
133
|
-
const recovery = facts.recoverySnapshot;
|
|
134
|
-
const hasRecovery = recovery !== null;
|
|
135
119
|
|
|
136
|
-
if (!hasSession && !hasCheckpoints && !hasChainHistory
|
|
120
|
+
if (!hasSession && !hasCheckpoints && !hasChainHistory) return null;
|
|
137
121
|
|
|
138
122
|
const summary: string[] = [];
|
|
139
123
|
if (hasSession) summary.push(plural(facts.turnCount!, 'turn'));
|
|
@@ -142,24 +126,16 @@ export function buildResumeNotice(facts: ResumeNoticeFacts): string | null {
|
|
|
142
126
|
// without one) — never guessed when the manager is unavailable.
|
|
143
127
|
if (checkpointsKnown && (hasSession || hasCheckpoints)) summary.push(plural(checkpointCount, 'checkpoint'));
|
|
144
128
|
if (hasChainHistory) summary.push(`last chain: ${facts.lastChainOutcome}`);
|
|
145
|
-
// Reported whenever a live snapshot exists, independent of whether a
|
|
146
|
-
// restore command can be advertised for it (see the hint below).
|
|
147
|
-
if (hasRecovery) summary.push('recovery snapshot found');
|
|
148
129
|
|
|
149
130
|
const lead = hasSession ? 'Previous session found' : 'Workspace history found';
|
|
150
131
|
let notice = `${lead}: ${summary.join(', ')}`;
|
|
151
132
|
|
|
152
133
|
const hints: string[] = [];
|
|
153
|
-
//
|
|
154
|
-
|
|
134
|
+
// Lead with the zero-typing picker (/resume), keep the direct exact-id
|
|
135
|
+
// form as a secondary hint — see the header doc for why both are honest.
|
|
136
|
+
if (hasSession) hints.push(`/resume to continue (or /session resume ${facts.lastSessionId} directly)`);
|
|
155
137
|
if (hasCheckpoints) hints.push('/checkpoints to browse');
|
|
156
138
|
if (facts.memoryAvailable) hints.push('/recall for memory');
|
|
157
|
-
// Only advertise the recovery snapshot's restore command when it is
|
|
158
|
-
// verified-resumable AND names a session other than the one already
|
|
159
|
-
// hinted above — otherwise this would just repeat the same command.
|
|
160
|
-
if (hasRecovery && recovery!.resumable && recovery!.sessionId !== facts.lastSessionId) {
|
|
161
|
-
hints.push(`/session resume ${recovery!.sessionId} to restore it`);
|
|
162
|
-
}
|
|
163
139
|
if (hints.length > 0) notice += ` — ${hints.join(' · ')}`;
|
|
164
140
|
|
|
165
141
|
return notice;
|
|
@@ -168,10 +144,8 @@ export function buildResumeNotice(facts: ResumeNoticeFacts): string | null {
|
|
|
168
144
|
// ─── Fact gathering (I/O) ────────────────────────────────────────────────────
|
|
169
145
|
|
|
170
146
|
export interface ResumeNoticeDeps {
|
|
171
|
-
|
|
172
|
-
readonly
|
|
173
|
-
/** Surface root used by session persistence — the TUI always uses 'tui'. */
|
|
174
|
-
readonly surfaceRoot: string;
|
|
147
|
+
/** The app's declare-once session-storage handle — the same one the runtime writes the pointer through. */
|
|
148
|
+
readonly surface: SessionSurface;
|
|
175
149
|
/** Only `load()` is needed — kept narrow for testability. */
|
|
176
150
|
readonly sessionManager: Pick<SessionManager, 'load'>;
|
|
177
151
|
/** Undefined when checkpoints are not wired up in this session at all. Only `list()` is needed. */
|
|
@@ -191,12 +165,8 @@ export interface ResumeNoticeDeps {
|
|
|
191
165
|
* session file is missing/corrupt — never a claim about a session that
|
|
192
166
|
* cannot actually be resumed.
|
|
193
167
|
*/
|
|
194
|
-
function readLastSessionTurns(deps: Pick<ResumeNoticeDeps, '
|
|
195
|
-
const lastSessionId = readLastSessionPointer({
|
|
196
|
-
workingDirectory: deps.workingDirectory,
|
|
197
|
-
homeDirectory: deps.homeDirectory,
|
|
198
|
-
surfaceRoot: deps.surfaceRoot,
|
|
199
|
-
});
|
|
168
|
+
function readLastSessionTurns(deps: Pick<ResumeNoticeDeps, 'surface' | 'sessionManager'>): { turnCount: number; lastSessionId: string } | null {
|
|
169
|
+
const lastSessionId = readLastSessionPointer({ surface: deps.surface });
|
|
200
170
|
if (!lastSessionId) return null;
|
|
201
171
|
try {
|
|
202
172
|
const { messages } = deps.sessionManager.load(lastSessionId);
|
|
@@ -209,36 +179,6 @@ function readLastSessionTurns(deps: Pick<ResumeNoticeDeps, 'workingDirectory' |
|
|
|
209
179
|
}
|
|
210
180
|
}
|
|
211
181
|
|
|
212
|
-
/**
|
|
213
|
-
* Whether a live recovery snapshot exists, and whether its exact
|
|
214
|
-
* session id is also resumable through the real `/session resume <id>` path.
|
|
215
|
-
* A real load attempt (not a guess) decides resumability: `sessionManager`
|
|
216
|
-
* and `checkRecoveryFile`'s recovery directory are genuinely separate
|
|
217
|
-
* on-disk stores (see resume-notice.ts's header doc), so a snapshot whose
|
|
218
|
-
* session never completed a full turn will not be in the session store yet.
|
|
219
|
-
* Returns null when there is no live snapshot at all.
|
|
220
|
-
*/
|
|
221
|
-
function readRecoverySnapshot(
|
|
222
|
-
deps: Pick<ResumeNoticeDeps, 'workingDirectory' | 'homeDirectory' | 'sessionManager'>,
|
|
223
|
-
): { sessionId: string; resumable: boolean } | null {
|
|
224
|
-
// No surfaceRoot: the recovery file lives in the SHARED .goodvibes/recovery
|
|
225
|
-
// directory, the same way main.ts's writeRecoveryFile call and the SDK's
|
|
226
|
-
// own getContinuitySnapshot() read it — unlike the session store /
|
|
227
|
-
// last-session pointer above, which IS scoped to surfaceRoot 'tui'.
|
|
228
|
-
const info = checkRecoveryFile({
|
|
229
|
-
workingDirectory: deps.workingDirectory,
|
|
230
|
-
homeDirectory: deps.homeDirectory,
|
|
231
|
-
});
|
|
232
|
-
if (!info) return null;
|
|
233
|
-
let resumable = true;
|
|
234
|
-
try {
|
|
235
|
-
deps.sessionManager.load(info.sessionId);
|
|
236
|
-
} catch {
|
|
237
|
-
resumable = false;
|
|
238
|
-
}
|
|
239
|
-
return { sessionId: info.sessionId, resumable };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
182
|
async function readCheckpointCount(mgr: ResumeNoticeDeps['checkpointManager']): Promise<number | null> {
|
|
243
183
|
if (!mgr) return null;
|
|
244
184
|
try {
|
|
@@ -253,18 +193,14 @@ async function readCheckpointCount(mgr: ResumeNoticeDeps['checkpointManager']):
|
|
|
253
193
|
/**
|
|
254
194
|
* Gather real facts from disk/services and, if there is anything to report,
|
|
255
195
|
* print ONE compact system message via `deps.router.high`. No-op (and no
|
|
256
|
-
* message) when there is no prior session, no checkpoints, no chain
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* startup: it never mutates the conversation, never touches the journal, and
|
|
260
|
-
* never deletes the snapshot file — restoring it is always the user's
|
|
261
|
-
* explicit call (via the advertised /session resume <id>, when one exists).
|
|
196
|
+
* message) when there is no prior session, no checkpoints, and no chain
|
|
197
|
+
* history — a fresh working directory stays quiet. Nothing here ever mutates
|
|
198
|
+
* the conversation: this is a report, not a restore.
|
|
262
199
|
*/
|
|
263
200
|
export async function announceResumeState(deps: ResumeNoticeDeps): Promise<void> {
|
|
264
201
|
const session = readLastSessionTurns(deps);
|
|
265
202
|
const checkpointCount = await readCheckpointCount(deps.checkpointManager);
|
|
266
203
|
const lastChain = mostRecentChain(deps.chainHistory);
|
|
267
|
-
const recoverySnapshot = readRecoverySnapshot(deps);
|
|
268
204
|
|
|
269
205
|
const notice = buildResumeNotice({
|
|
270
206
|
turnCount: session?.turnCount ?? null,
|
|
@@ -272,7 +208,6 @@ export async function announceResumeState(deps: ResumeNoticeDeps): Promise<void>
|
|
|
272
208
|
checkpointCount,
|
|
273
209
|
lastChainOutcome: lastChain ? describeChainOutcome(lastChain) : null,
|
|
274
210
|
memoryAvailable: deps.memoryAvailable,
|
|
275
|
-
recoverySnapshot,
|
|
276
211
|
});
|
|
277
212
|
|
|
278
213
|
if (notice) deps.router.high(notice);
|
package/src/runtime/services.ts
CHANGED
|
@@ -37,7 +37,9 @@ import {
|
|
|
37
37
|
type RuntimeEventBus, DistributedRuntimeManager, RemoteRunnerRegistry, RemoteSupervisor, IntegrationHelperService,
|
|
38
38
|
IdempotencyStore, ComponentHealthMonitor, WorktreeRegistry, SandboxSessionRegistry, createShellPathService,
|
|
39
39
|
type ShellPathService, type FeatureFlagManager, createFeatureFlagManager, PolicyRuntimeState,
|
|
40
|
+
type SessionSurface,
|
|
40
41
|
} from '@/runtime/index.ts';
|
|
42
|
+
import { createSessionStorageServices } from './session-storage-services.ts';
|
|
41
43
|
import { VoiceProviderRegistry, VoiceService, ensureBuiltinVoiceProviders } from '@pellux/goodvibes-sdk/platform/voice';
|
|
42
44
|
import { CacheRegistry, PauseController, type MemoryGovernor } from '@pellux/goodvibes-sdk/platform/runtime/memory';
|
|
43
45
|
import { wireMemoryGovernance } from './memory-governance-services.ts';
|
|
@@ -63,7 +65,7 @@ import { type ArchivableProcessRegistry } from '@pellux/goodvibes-sdk/platform/r
|
|
|
63
65
|
import { createFleetServices } from './fleet-services.ts';
|
|
64
66
|
import { createWorkstreamServices, type OrchestrationEngine, type WorkstreamCommandService } from './workstream-services.ts';
|
|
65
67
|
import { wireFleetNeedsInputPush } from './fleet-needs-input-push.ts';
|
|
66
|
-
import { codeIndexDbPath, createCodeIndexServices, isCodeInjectionSettingEnabled } from './code-index-services.ts';
|
|
68
|
+
import { codeIndexDbPath, createCodeIndexServices, createStoreRerooter, isCodeInjectionSettingEnabled } from './code-index-services.ts';
|
|
67
69
|
import type { WorkPlanStore } from '../work-plans/work-plan-store.ts';
|
|
68
70
|
import { registerDaemonHandlers, type DaemonHandlerSurfaces } from '../daemon/handlers/index.ts';
|
|
69
71
|
import type { HandlerContext, HandlerLogger } from '../daemon/handlers/context.ts';
|
|
@@ -99,6 +101,8 @@ export interface RuntimeServicesOptions {
|
|
|
99
101
|
export interface RuntimeServices {
|
|
100
102
|
readonly workingDirectory: string;
|
|
101
103
|
readonly homeDirectory: string;
|
|
104
|
+
/** The declare-once session-storage handle every session reader and writer threads through — see session-storage-services.ts. */
|
|
105
|
+
readonly surface: SessionSurface;
|
|
102
106
|
readonly shellPaths: ShellPathService;
|
|
103
107
|
readonly configManager: ConfigManager;
|
|
104
108
|
readonly featureFlags: FeatureFlagManager;
|
|
@@ -231,6 +235,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
231
235
|
workingDirectory,
|
|
232
236
|
homeDirectory,
|
|
233
237
|
});
|
|
238
|
+
// Built before anything that touches session state — see session-storage-services.ts.
|
|
239
|
+
const { surface, sessionManager } = createSessionStorageServices({ workingDirectory, homeDirectory });
|
|
234
240
|
const workspaceTrustManager = new WorkspaceTrustManager({ shellPaths });
|
|
235
241
|
const configManager = options.configManager;
|
|
236
242
|
const featureFlags = options.featureFlags ?? createFeatureFlagManager();
|
|
@@ -318,9 +324,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
318
324
|
});
|
|
319
325
|
const profileManager = new ProfileManager(shellPaths.resolveUserPath('tui', 'profiles'));
|
|
320
326
|
const bookmarkManager = new BookmarkManager(shellPaths.resolveUserPath('tui', 'bookmarks'));
|
|
321
|
-
const sessionManager = new SessionManager(workingDirectory, { surfaceRoot: 'tui' });
|
|
322
327
|
const sessionOrchestration = new CrossSessionTaskRegistry(
|
|
323
|
-
|
|
328
|
+
join(surface.sessionsDir, 'task-graph.json'),
|
|
324
329
|
);
|
|
325
330
|
const hookActivityTracker = new HookActivityTracker();
|
|
326
331
|
const watcherRegistry = new WatcherRegistry({
|
|
@@ -564,7 +569,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
564
569
|
// Store snapshots + durable remembered-approval rules + the live credential
|
|
565
570
|
// chain, mirroring the SDK composition — see durability-services.ts.
|
|
566
571
|
const { storeSnapshotScheduler, userPermissionRuleStore } = createDurabilityServices({
|
|
567
|
-
configManager, secretsManager, providerRegistry, memoryDbPath, codeIndexDbPath: codeIndexDbPath(workingDirectory),
|
|
572
|
+
configManager, secretsManager, providerRegistry, memoryDbPath, codeIndexDbPath: codeIndexDbPath(workingDirectory), surface, shellPaths, // + retention-sweep roots & live config watch (mirrors the SDK)
|
|
568
573
|
});
|
|
569
574
|
const codeInjectionOrchestratorDeps = { codeIndex: codeIndexStore, isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager), codeIndexReindexScheduler }; // Code-injection seam (agent here; main via orchestrator-core-services.ts)
|
|
570
575
|
const { processRegistry } = createFleetServices({ // Shared archive-aware fleet registry (+ daemon observed rows) — see fleet-services.ts
|
|
@@ -578,7 +583,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
578
583
|
observeExternalAgents: options.observeExternalAgents, providerRegistry, // observeExternalAgents is daemon-side only
|
|
579
584
|
});
|
|
580
585
|
const modeManager = new ModeManager(); const fileUndoManager = new FileUndoManager();
|
|
581
|
-
const workspaceCheckpointManager = createWorkspaceCheckpointing({ workspaceRoot: workingDirectory, runtimeBus: options.runtimeBus, configManager });
|
|
586
|
+
const workspaceCheckpointManager = createWorkspaceCheckpointing({ workspaceRoot: workingDirectory, surface, runtimeBus: options.runtimeBus, configManager });
|
|
582
587
|
// memory-consolidation honors governor backpressure: it ticks only when idle
|
|
583
588
|
// AND the 'memory-consolidation' job is not paused AND expensive work is
|
|
584
589
|
// admitted (mirrors the SDK's own createRuntimeServices idle gate).
|
|
@@ -610,8 +615,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
610
615
|
// Terminal-shell wrapper over the SDK registerGatewayVerbGroups (gateway-verbs.ts); checkin.*/fleet-needs-input/pairing.* register only when their deps are present. memoryGovernor lights up ops.memory.get; voiceSetup lights up voice.local.status/install.
|
|
611
616
|
attachWsOnlyGatewayVerbHandlers(gatewayMethods, { processRegistry, workspaceCheckpointManager, conversationRewindPort: createSessionConversationRewindPort(), sessionBroker, secretsManager, stepUpService, approvalBroker, requestApproval: (input) => approvalBroker.requestApproval(input), watcherRegistry, userPermissionRuleStore, shellPaths, configManager, runtimeStore: options.runtimeStore, channelDeliveryRouter, providerRegistry, automationManager, sessionLister: sessionBroker, sessionIntake: sessionBroker, workingDirectory, memoryRegistry, pairingTokens, sessionLiveTurnControls, powerManager, memoryGovernor, voiceSetup, relayAvailable: () => configManager.get('relay.enabled') === true, pairingWebOrigin: () => resolvePairingWebOrigin(configManager).origin, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
|
|
612
617
|
const integrationHelpers = new IntegrationHelperService({
|
|
613
|
-
|
|
614
|
-
homeDirectory,
|
|
618
|
+
surface, // surface-scoped: continuity's recovery-file check must read the SAME paths the app writes with, not the unscoped legacy pair.
|
|
615
619
|
runtimeStore: options.runtimeStore,
|
|
616
620
|
runtimeBus: options.runtimeBus,
|
|
617
621
|
configManager,
|
|
@@ -636,7 +640,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
636
640
|
// Exec stuck on a terminal prompt rides the approval broker; the typed answer feeds the continuing run. Built once and shared (like localhostFetchApproval) so the tool registry AND every setDependencies site — incl. the post-clobber rewire in bootstrap-core — install the SAME handler; otherwise a wholesale replace drops it and interactive prompts hang.
|
|
637
641
|
const execPromptAnswerHandler = buildExecPromptAnswerHandler({ requestApproval: (input) => approvalBroker.requestApproval(input) });
|
|
638
642
|
agentOrchestrator.setDependencies({
|
|
639
|
-
surfaceRoot:
|
|
643
|
+
surfaceRoot: surface.surfaceRoot,
|
|
640
644
|
execPromptAnswerHandler,
|
|
641
645
|
localhostFetchApproval,
|
|
642
646
|
fileCache,
|
|
@@ -678,6 +682,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
678
682
|
return {
|
|
679
683
|
workingDirectory,
|
|
680
684
|
homeDirectory,
|
|
685
|
+
surface,
|
|
681
686
|
shellPaths,
|
|
682
687
|
workspaceTrustManager,
|
|
683
688
|
configManager,
|
|
@@ -785,13 +790,6 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
785
790
|
fileUndoManager,
|
|
786
791
|
workspaceCheckpointManager,
|
|
787
792
|
integrationHelpers,
|
|
788
|
-
|
|
789
|
-
// The memory store is the home-scoped canonical cross-surface store and
|
|
790
|
-
// deliberately does NOT reroot per-project (that would re-silo memory back
|
|
791
|
-
// into per-project stores). Only working-tree-bound stores (code index,
|
|
792
|
-
// project index) reroot.
|
|
793
|
-
await codeIndexStore.reroot(newWorkingDir, codeIndexDbPath(newWorkingDir));
|
|
794
|
-
await projectIndex.reroot(newWorkingDir);
|
|
795
|
-
},
|
|
793
|
+
rerootStores: createStoreRerooter({ codeIndexStore, projectIndex }),
|
|
796
794
|
};
|
|
797
795
|
}
|