@selesai/code 0.11.0 → 0.13.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 +26 -0
- package/dist/cli/startup-ui.js +2 -1
- package/dist/core/agent-session.d.ts +12 -1
- package/dist/core/agent-session.js +77 -9
- package/dist/core/compaction/branch-summarization.js +4 -3
- package/dist/core/compaction/compaction.d.ts +5 -0
- package/dist/core/compaction/compaction.js +19 -4
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.js +48 -1
- package/dist/core/extensions/types.d.ts +18 -1
- package/dist/core/llama/client.d.ts +6 -0
- package/dist/core/llama/client.js +7 -0
- package/dist/core/llama/provider.d.ts +3 -1
- package/dist/core/llama/provider.js +28 -6
- package/dist/core/session-manager.js +4 -2
- package/dist/core/settings-manager.d.ts +8 -1
- package/dist/core/settings-manager.js +17 -0
- package/dist/core/usage-totals.d.ts +2 -0
- package/dist/core/usage-totals.js +10 -0
- package/dist/defaults/models.json +32 -9
- package/dist/extensions/pi-zentui/extensions/zentui/format.ts +2 -1
- package/dist/extensions/pi-zentui/test/format.test.ts +15 -1
- package/dist/extensions/tokenin-onboarding.ts +112 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/main.js +2 -0
- package/dist/modes/interactive/components/footer.js +2 -2
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.js +59 -43
- package/dist/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-types.d.ts +12 -0
- package/dist/utils/shell.js +6 -4
- package/docs/settings.md +4 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.0] - 2026-08-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Sub-cent cost display.** Cost labels no longer round small per-call costs to `$0.000`. The footer, zentui footer, and usage popup now format costs adaptively: 3 decimals at $1+, 4 decimals at $0.01+, and 6 decimals below, so gateway-reconciled costs like `$0.000336` are visible.
|
|
9
|
+
|
|
10
|
+
## [0.12.0] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`/tokenin usage` subcommand.** The `tokenin-onboarding` extension now queries the LiteLLM `/key/info` endpoint with the active Token-In token and shows a boxed spend/budget/remaining/reset display with a progress bar. Run `/tokenin usage` to check quota without leaving the session.
|
|
14
|
+
- **Terminal capability overrides.** New `terminal.images` (`"kitty"`, `"iterm2"`, `"auto"`, or `false`), `terminal.trueColor`, and `terminal.hyperlinks` settings let you pin terminal capabilities instead of relying on auto-detection.
|
|
15
|
+
- **`fullscreenCopyOnSelect` setting.** New setting (default `true`) and `/settings` toggle: automatically copy selected text in fullscreen mode; disable to copy selections with Ctrl+X.
|
|
16
|
+
- **`ui_prompt_start` / `ui_prompt_end` extension events.** Extensions can now observe when the agent starts and stops waiting on a blocking user-facing UI prompt (`select`, `confirm`, `input`, `editor`, `custom`).
|
|
17
|
+
- **llama.cpp router autoload.** Unloaded preset models are now selectable when the llama.cpp router has `models_autoload` enabled, so they load on first use instead of being hidden.
|
|
18
|
+
- **RPC `clear_queue` command.** RPC clients can clear the pending steering/follow-up queue and get back the dropped items.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **Custom message ordering during streaming.** Context-only custom messages queued while the agent is streaming are now flushed at turn end (after tool results are in) instead of being appended between a tool call and its result, which some providers reject on replay.
|
|
22
|
+
- **Auto-compaction before the next assistant response.** The session now checks the context threshold before preparing the next turn and compacts when needed, so long conversations stay within the model window.
|
|
23
|
+
- **Persisted default model joins scoped model lists.** When a scoped model list (`--models`) is active, the persisted default model is added to the scope and to `enabledModels` so it stays reachable.
|
|
24
|
+
- **Model catalog updates.** Celestial, DeepSeek, and GPT models now declare `compat.requiresReasoningContentOnAssistantMessages` where required.
|
|
25
|
+
- **Windows process cleanup.** `taskkill` is invoked via the trusted System32 path instead of relying on `PATH`, and failed spawns no longer crash Node.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Footer provider count after model switch.** The interactive footer's available-provider count now refreshes immediately after switching models in the model selector.
|
|
29
|
+
- **Summarization length-stop guard.** A summarization response that hits the token cap (partial text) is now treated as a failure and never persisted as a session checkpoint.
|
|
30
|
+
|
|
5
31
|
## [0.11.0] - 2026-08-29
|
|
6
32
|
|
|
7
33
|
### Added
|
package/dist/cli/startup-ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProcessTerminal, setKeybindings, TuiMainScreen } from "@earendil-works/pi-tui";
|
|
1
|
+
import { ProcessTerminal, setCapabilityOverrides, setKeybindings, TuiMainScreen, } from "@earendil-works/pi-tui";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { ENV_AGENT_DIR, getAgentDir, getFirstRunMarkerPath, markFirstRunComplete } from "../config.js";
|
|
4
4
|
import { KeybindingsManager } from "../core/keybindings.js";
|
|
@@ -43,6 +43,7 @@ async function loadStartupThemes(settingsManager) {
|
|
|
43
43
|
return loadThemes(resolvedPaths.themes);
|
|
44
44
|
}
|
|
45
45
|
export async function createStartupTui(settingsManager) {
|
|
46
|
+
setCapabilityOverrides(settingsManager.getTerminalCapabilityOverrides());
|
|
46
47
|
setRegisteredThemes(await loadStartupThemes(settingsManager));
|
|
47
48
|
const terminalTheme = detectTerminalBackgroundFromEnv().theme;
|
|
48
49
|
initTheme(resolveThemeSetting(settingsManager.getThemeSetting(), terminalTheme) ?? terminalTheme);
|
|
@@ -218,6 +218,8 @@ export declare class AgentSession {
|
|
|
218
218
|
private _followUpMessages;
|
|
219
219
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
220
220
|
private _pendingNextTurnMessages;
|
|
221
|
+
/** Context-only custom messages queued during a run, flushed once the current turn's tool results are in. */
|
|
222
|
+
private _pendingCustomMessages;
|
|
221
223
|
private _compactionAbortController;
|
|
222
224
|
private _autoCompactionAbortController;
|
|
223
225
|
private _overflowRecoveryAttempted;
|
|
@@ -266,6 +268,7 @@ export declare class AgentSession {
|
|
|
266
268
|
* happens here instead of in wrappers.
|
|
267
269
|
*/
|
|
268
270
|
private _installAgentToolHooks;
|
|
271
|
+
private _compactBeforeNextAssistantResponse;
|
|
269
272
|
private _installAgentNextTurnRefresh;
|
|
270
273
|
/** Emit an event to all listeners */
|
|
271
274
|
private _emit;
|
|
@@ -412,8 +415,9 @@ export declare class AgentSession {
|
|
|
412
415
|
/**
|
|
413
416
|
* Send a custom message to the session. Creates a CustomMessageEntry.
|
|
414
417
|
*
|
|
415
|
-
* Handles
|
|
418
|
+
* Handles four cases:
|
|
416
419
|
* - Streaming: queues message, processed when loop pulls from queue
|
|
420
|
+
* - Streaming + triggerTurn false: appended to state/session once the current turn ends
|
|
417
421
|
* - Not streaming + triggerTurn: appends to state/session, starts new turn
|
|
418
422
|
* - Not streaming + no trigger: appends to state/session, no turn
|
|
419
423
|
*
|
|
@@ -425,6 +429,12 @@ export declare class AgentSession {
|
|
|
425
429
|
triggerTurn?: boolean;
|
|
426
430
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
427
431
|
}): Promise<void>;
|
|
432
|
+
private _appendCustomMessage;
|
|
433
|
+
/**
|
|
434
|
+
* Append custom messages queued while the agent was running.
|
|
435
|
+
* Called once the current turn's tool results are in agent state and session history.
|
|
436
|
+
*/
|
|
437
|
+
private _flushPendingCustomMessages;
|
|
428
438
|
private _captionImagesForCurrentModel;
|
|
429
439
|
/**
|
|
430
440
|
* Build a small, text-only slice of the most recent conversation to give the
|
|
@@ -485,6 +495,7 @@ export declare class AgentSession {
|
|
|
485
495
|
* @throws Error if no auth is configured for the model
|
|
486
496
|
*/
|
|
487
497
|
setModel(model: Model<any>, options?: ModelMutationOptions): Promise<void>;
|
|
498
|
+
private _addPersistedDefaultToNonEmptyScope;
|
|
488
499
|
/**
|
|
489
500
|
* Cycle to next/previous model.
|
|
490
501
|
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
@@ -102,6 +102,8 @@ export class AgentSession {
|
|
|
102
102
|
_followUpMessages = [];
|
|
103
103
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
104
104
|
_pendingNextTurnMessages = [];
|
|
105
|
+
/** Context-only custom messages queued during a run, flushed once the current turn's tool results are in. */
|
|
106
|
+
_pendingCustomMessages = [];
|
|
105
107
|
// Compaction state
|
|
106
108
|
_compactionAbortController = undefined;
|
|
107
109
|
_autoCompactionAbortController = undefined;
|
|
@@ -269,18 +271,33 @@ export class AgentSession {
|
|
|
269
271
|
};
|
|
270
272
|
};
|
|
271
273
|
}
|
|
274
|
+
async _compactBeforeNextAssistantResponse(context) {
|
|
275
|
+
const model = this.model;
|
|
276
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
277
|
+
if (!model ||
|
|
278
|
+
model.contextWindow <= 0 ||
|
|
279
|
+
!shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings)) {
|
|
280
|
+
return context;
|
|
281
|
+
}
|
|
282
|
+
await this._runAutoCompaction("threshold", false);
|
|
283
|
+
return {
|
|
284
|
+
...context,
|
|
285
|
+
messages: this.agent.state.messages.slice(),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
272
288
|
_installAgentNextTurnRefresh() {
|
|
273
289
|
const previousPrepareNextTurnWithContext = this.agent.prepareNextTurnWithContext ??
|
|
274
290
|
(this.agent.prepareNextTurn
|
|
275
291
|
? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
|
|
276
292
|
: undefined);
|
|
277
293
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
278
|
-
const
|
|
279
|
-
const
|
|
294
|
+
const context = await this._compactBeforeNextAssistantResponse(turn.context);
|
|
295
|
+
const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
|
|
296
|
+
const nextContext = previousSnapshot?.context ?? context;
|
|
280
297
|
return {
|
|
281
298
|
...previousSnapshot,
|
|
282
299
|
context: {
|
|
283
|
-
...
|
|
300
|
+
...nextContext,
|
|
284
301
|
systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
|
|
285
302
|
tools: this.agent.state.tools.slice(),
|
|
286
303
|
},
|
|
@@ -436,6 +453,14 @@ export class AgentSession {
|
|
|
436
453
|
}
|
|
437
454
|
}
|
|
438
455
|
}
|
|
456
|
+
// A turn ends after its assistant message and every tool result has been appended,
|
|
457
|
+
// so this is the first point in the run where a context-only custom message can be
|
|
458
|
+
// inserted without landing between a tool call and its result. Flushing after the
|
|
459
|
+
// extension and listener dispatch above also picks up messages that turn_end
|
|
460
|
+
// handlers queued.
|
|
461
|
+
if (event.type === "turn_end") {
|
|
462
|
+
this._flushPendingCustomMessages();
|
|
463
|
+
}
|
|
439
464
|
};
|
|
440
465
|
_willRetryAfterAgentEnd(event) {
|
|
441
466
|
const settings = this.settingsManager.getRetrySettings();
|
|
@@ -813,6 +838,7 @@ export class AgentSession {
|
|
|
813
838
|
finally {
|
|
814
839
|
this._systemPromptOverride = undefined;
|
|
815
840
|
this._flushPendingBashMessages();
|
|
841
|
+
this._flushPendingCustomMessages();
|
|
816
842
|
await this._emitAgentSettled();
|
|
817
843
|
}
|
|
818
844
|
}
|
|
@@ -908,8 +934,9 @@ export class AgentSession {
|
|
|
908
934
|
preflightResult?.(true);
|
|
909
935
|
return;
|
|
910
936
|
}
|
|
911
|
-
// Flush any pending bash messages before the new prompt
|
|
937
|
+
// Flush any pending bash and custom messages before the new prompt
|
|
912
938
|
this._flushPendingBashMessages();
|
|
939
|
+
this._flushPendingCustomMessages();
|
|
913
940
|
// Validate model
|
|
914
941
|
if (!this.model) {
|
|
915
942
|
throw new Error(formatNoModelSelectedMessage());
|
|
@@ -1138,8 +1165,9 @@ export class AgentSession {
|
|
|
1138
1165
|
/**
|
|
1139
1166
|
* Send a custom message to the session. Creates a CustomMessageEntry.
|
|
1140
1167
|
*
|
|
1141
|
-
* Handles
|
|
1168
|
+
* Handles four cases:
|
|
1142
1169
|
* - Streaming: queues message, processed when loop pulls from queue
|
|
1170
|
+
* - Streaming + triggerTurn false: appended to state/session once the current turn ends
|
|
1143
1171
|
* - Not streaming + triggerTurn: appends to state/session, starts new turn
|
|
1144
1172
|
* - Not streaming + no trigger: appends to state/session, no turn
|
|
1145
1173
|
*
|
|
@@ -1171,11 +1199,34 @@ export class AgentSession {
|
|
|
1171
1199
|
else if (options?.triggerTurn) {
|
|
1172
1200
|
await this._runAgentPrompt(appMessage);
|
|
1173
1201
|
}
|
|
1202
|
+
else if (this.isStreaming) {
|
|
1203
|
+
// Appending now would put the message between an assistant tool call and its
|
|
1204
|
+
// result, which providers that validate message order reject on replay. Defer
|
|
1205
|
+
// to the end of the turn. Nothing is emitted yet: message events must not
|
|
1206
|
+
// describe messages the session tree does not contain.
|
|
1207
|
+
this._pendingCustomMessages.push(appMessage);
|
|
1208
|
+
}
|
|
1174
1209
|
else {
|
|
1175
|
-
this.
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1210
|
+
this._appendCustomMessage(appMessage);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
_appendCustomMessage(appMessage) {
|
|
1214
|
+
this.agent.state.messages.push(appMessage);
|
|
1215
|
+
this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details);
|
|
1216
|
+
this._emit({ type: "message_start", message: appMessage });
|
|
1217
|
+
this._emit({ type: "message_end", message: appMessage });
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Append custom messages queued while the agent was running.
|
|
1221
|
+
* Called once the current turn's tool results are in agent state and session history.
|
|
1222
|
+
*/
|
|
1223
|
+
_flushPendingCustomMessages() {
|
|
1224
|
+
if (this._pendingCustomMessages.length === 0)
|
|
1225
|
+
return;
|
|
1226
|
+
const pending = this._pendingCustomMessages;
|
|
1227
|
+
this._pendingCustomMessages = [];
|
|
1228
|
+
for (const appMessage of pending) {
|
|
1229
|
+
this._appendCustomMessage(appMessage);
|
|
1179
1230
|
}
|
|
1180
1231
|
}
|
|
1181
1232
|
/**
|
|
@@ -1448,10 +1499,25 @@ export class AgentSession {
|
|
|
1448
1499
|
this.sessionManager.appendModelChange(model.provider, model.id);
|
|
1449
1500
|
if (options.persist) {
|
|
1450
1501
|
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
1502
|
+
this._addPersistedDefaultToNonEmptyScope(model);
|
|
1451
1503
|
}
|
|
1452
1504
|
this.setThinkingLevel(thinkingLevel);
|
|
1453
1505
|
await this._emitModelSelect(model, previousModel, "set");
|
|
1454
1506
|
}
|
|
1507
|
+
_addPersistedDefaultToNonEmptyScope(model) {
|
|
1508
|
+
if (this._scopedModels.length === 0)
|
|
1509
|
+
return;
|
|
1510
|
+
if (this._scopedModels.some((scoped) => modelsAreEqual(scoped.model, model)))
|
|
1511
|
+
return;
|
|
1512
|
+
this._scopedModels = [...this._scopedModels, { model }];
|
|
1513
|
+
const enabledModels = this.settingsManager.getEnabledModels();
|
|
1514
|
+
if (!enabledModels?.length)
|
|
1515
|
+
return;
|
|
1516
|
+
const modelReference = `${model.provider}/${model.id}`;
|
|
1517
|
+
if (enabledModels.some((pattern) => pattern.toLowerCase() === modelReference.toLowerCase()))
|
|
1518
|
+
return;
|
|
1519
|
+
this.settingsManager.setEnabledModels([...enabledModels, modelReference]);
|
|
1520
|
+
}
|
|
1455
1521
|
/**
|
|
1456
1522
|
* Cycle to next/previous model.
|
|
1457
1523
|
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
@@ -1485,6 +1551,7 @@ export class AgentSession {
|
|
|
1485
1551
|
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
|
|
1486
1552
|
if (options.persist) {
|
|
1487
1553
|
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
|
|
1554
|
+
this._addPersistedDefaultToNonEmptyScope(next.model);
|
|
1488
1555
|
}
|
|
1489
1556
|
// Apply thinking level.
|
|
1490
1557
|
// - Explicit scoped model thinking level overrides current session level
|
|
@@ -1510,6 +1577,7 @@ export class AgentSession {
|
|
|
1510
1577
|
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
|
|
1511
1578
|
if (options.persist) {
|
|
1512
1579
|
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
1580
|
+
this._addPersistedDefaultToNonEmptyScope(nextModel);
|
|
1513
1581
|
}
|
|
1514
1582
|
// Re-clamp thinking level for new model's capabilities
|
|
1515
1583
|
this.setThinkingLevel(thinkingLevel);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { contentText } from "@earendil-works/pi-ai";
|
|
8
8
|
import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
9
|
-
import { completeSummarization, estimateTokens } from "./compaction.js";
|
|
9
|
+
import { completeSummarization, estimateTokens, getSummarizationFailure } from "./compaction.js";
|
|
10
10
|
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Entry Collection
|
|
@@ -227,8 +227,9 @@ export async function generateBranchSummary(entries, options) {
|
|
|
227
227
|
if (response.stopReason === "aborted") {
|
|
228
228
|
return { aborted: true };
|
|
229
229
|
}
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
const failure = getSummarizationFailure(response, "Branch summarization");
|
|
231
|
+
if (failure) {
|
|
232
|
+
return { error: failure };
|
|
232
233
|
}
|
|
233
234
|
let summary = contentText(response.content);
|
|
234
235
|
// Prepend preamble to provide context about the branch summary
|
|
@@ -90,6 +90,11 @@ export interface CutPointResult {
|
|
|
90
90
|
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
|
91
91
|
*/
|
|
92
92
|
export declare function findCutPoint(entries: SessionEntry[], startIndex: number, endIndex: number, keepRecentTokens: number): CutPointResult;
|
|
93
|
+
/**
|
|
94
|
+
* Returns an error message when a summarization response cannot safely be persisted.
|
|
95
|
+
* A length stop contains partial text and must not become a session checkpoint.
|
|
96
|
+
*/
|
|
97
|
+
export declare function getSummarizationFailure(response: AssistantMessage, label: string): string | undefined;
|
|
93
98
|
/**
|
|
94
99
|
* Shared choke point for every compaction/branch-summary summarization call. Wraps the
|
|
95
100
|
* single LLM call in {@link retryAssistantCall} so transient stream drops (e.g.
|
|
@@ -423,6 +423,19 @@ Use this EXACT format:
|
|
|
423
423
|
- [Preserve important context, add new if needed]
|
|
424
424
|
|
|
425
425
|
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
426
|
+
/**
|
|
427
|
+
* Returns an error message when a summarization response cannot safely be persisted.
|
|
428
|
+
* A length stop contains partial text and must not become a session checkpoint.
|
|
429
|
+
*/
|
|
430
|
+
export function getSummarizationFailure(response, label) {
|
|
431
|
+
if (response.stopReason === "error") {
|
|
432
|
+
return `${label} failed: ${response.errorMessage || "Unknown error"}`;
|
|
433
|
+
}
|
|
434
|
+
if (response.stopReason === "length") {
|
|
435
|
+
return `${label} failed: generation hit the token cap and the summary is incomplete`;
|
|
436
|
+
}
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
426
439
|
function createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel) {
|
|
427
440
|
const options = { maxTokens, signal, apiKey, headers, env };
|
|
428
441
|
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
|
|
@@ -483,8 +496,9 @@ export async function generateSummaryWithUsage(currentMessages, model, reserveTo
|
|
|
483
496
|
];
|
|
484
497
|
const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel);
|
|
485
498
|
const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, streamFn, retry, callbacks);
|
|
486
|
-
|
|
487
|
-
|
|
499
|
+
const failure = getSummarizationFailure(response, "Summarization");
|
|
500
|
+
if (failure) {
|
|
501
|
+
throw new Error(failure);
|
|
488
502
|
}
|
|
489
503
|
const textContent = contentText(response.content);
|
|
490
504
|
return { text: textContent, usage: response.usage };
|
|
@@ -634,8 +648,9 @@ async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey,
|
|
|
634
648
|
},
|
|
635
649
|
];
|
|
636
650
|
const response = await completeSummarization(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel), streamFn, retry, callbacks);
|
|
637
|
-
|
|
638
|
-
|
|
651
|
+
const failure = getSummarizationFailure(response, "Turn prefix summarization");
|
|
652
|
+
if (failure) {
|
|
653
|
+
throw new Error(failure);
|
|
639
654
|
}
|
|
640
655
|
return {
|
|
641
656
|
text: contentText(response.content),
|
|
@@ -6,6 +6,6 @@ export type { SourceInfo } from "../source-info.ts";
|
|
|
6
6
|
export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.ts";
|
|
7
7
|
export type { ExtensionErrorListener, ForkHandler, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, SwitchSessionHandler, } from "./runner.ts";
|
|
8
8
|
export { ExtensionRunner } from "./runner.ts";
|
|
9
|
-
export type { AfterProviderResponseEvent, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditorFactory, EditToolCallEvent, EditToolResultEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, WriteToolResultEvent, } from "./types.ts";
|
|
9
|
+
export type { AfterProviderResponseEvent, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditorFactory, EditToolCallEvent, EditToolResultEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, WriteToolResultEvent, } from "./types.ts";
|
|
10
10
|
export { defineTool, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, } from "./types.ts";
|
|
11
11
|
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.ts";
|
|
@@ -72,6 +72,8 @@ export declare class ExtensionRunner {
|
|
|
72
72
|
private runtime;
|
|
73
73
|
private uiContext;
|
|
74
74
|
private mode;
|
|
75
|
+
private uiPromptDepth;
|
|
76
|
+
private activeUIPrompt;
|
|
75
77
|
private cwd;
|
|
76
78
|
private sessionManager;
|
|
77
79
|
private modelRegistry;
|
|
@@ -105,6 +107,9 @@ export declare class ExtensionRunner {
|
|
|
105
107
|
}): void;
|
|
106
108
|
bindCommandContext(actions?: ExtensionCommandContextActions): void;
|
|
107
109
|
setUIContext(uiContext?: ExtensionUIContext, mode?: ExtensionMode): void;
|
|
110
|
+
private wrapUIPromptContext;
|
|
111
|
+
private withUIPrompt;
|
|
112
|
+
private emitUIPromptEvent;
|
|
108
113
|
getUIContext(): ExtensionUIContext;
|
|
109
114
|
hasUI(): boolean;
|
|
110
115
|
getExtensionPaths(): string[];
|
|
@@ -122,6 +122,8 @@ export class ExtensionRunner {
|
|
|
122
122
|
runtime;
|
|
123
123
|
uiContext;
|
|
124
124
|
mode = "print";
|
|
125
|
+
uiPromptDepth = 0;
|
|
126
|
+
activeUIPrompt;
|
|
125
127
|
cwd;
|
|
126
128
|
sessionManager;
|
|
127
129
|
modelRegistry;
|
|
@@ -265,9 +267,54 @@ export class ExtensionRunner {
|
|
|
265
267
|
this.reloadHandler = async () => { };
|
|
266
268
|
}
|
|
267
269
|
setUIContext(uiContext, mode = "print") {
|
|
268
|
-
this.uiContext = uiContext
|
|
270
|
+
this.uiContext = uiContext ? this.wrapUIPromptContext(uiContext) : noOpUIContext;
|
|
269
271
|
this.mode = mode;
|
|
270
272
|
}
|
|
273
|
+
wrapUIPromptContext(ui) {
|
|
274
|
+
return {
|
|
275
|
+
...ui,
|
|
276
|
+
select: (title, options, opts) => this.withUIPrompt("select", title, () => ui.select(title, options, opts)),
|
|
277
|
+
multiselect: ui.multiselect
|
|
278
|
+
? (title, options, opts) => this.withUIPrompt("select", title, () => ui.multiselect(title, options, opts))
|
|
279
|
+
: undefined,
|
|
280
|
+
confirm: (title, message, opts) => this.withUIPrompt("confirm", title, () => ui.confirm(title, message, opts)),
|
|
281
|
+
input: (title, placeholder, opts) => this.withUIPrompt("input", title, () => ui.input(title, placeholder, opts)),
|
|
282
|
+
editor: (title, prefill) => this.withUIPrompt("editor", title, () => ui.editor(title, prefill)),
|
|
283
|
+
custom: (factory, options) => this.withUIPrompt("custom", undefined, () => ui.custom(factory, options)),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
withUIPrompt(kind, title, run) {
|
|
287
|
+
const outerPrompt = this.uiPromptDepth++ === 0;
|
|
288
|
+
if (outerPrompt) {
|
|
289
|
+
this.activeUIPrompt = { kind, title };
|
|
290
|
+
this.emitUIPromptEvent({ type: "ui_prompt_start", reason: "ui_prompt", kind, ...(title ? { title } : {}) });
|
|
291
|
+
}
|
|
292
|
+
const finish = () => {
|
|
293
|
+
if (--this.uiPromptDepth > 0)
|
|
294
|
+
return;
|
|
295
|
+
this.uiPromptDepth = 0;
|
|
296
|
+
const prompt = this.activeUIPrompt ?? { kind, title };
|
|
297
|
+
this.activeUIPrompt = undefined;
|
|
298
|
+
this.emitUIPromptEvent({
|
|
299
|
+
type: "ui_prompt_end",
|
|
300
|
+
reason: "ui_prompt",
|
|
301
|
+
kind: prompt.kind,
|
|
302
|
+
...(prompt.title ? { title: prompt.title } : {}),
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
try {
|
|
306
|
+
return run().finally(finish);
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
finish();
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
emitUIPromptEvent(event) {
|
|
314
|
+
queueMicrotask(() => {
|
|
315
|
+
void this.emit(event);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
271
318
|
getUIContext() {
|
|
272
319
|
return this.uiContext;
|
|
273
320
|
}
|
|
@@ -563,6 +563,21 @@ export interface AgentEndEvent {
|
|
|
563
563
|
export interface AgentSettledEvent {
|
|
564
564
|
type: "agent_settled";
|
|
565
565
|
}
|
|
566
|
+
export type UIPromptKind = "select" | "confirm" | "input" | "editor" | "custom";
|
|
567
|
+
/** Fired when Pi starts waiting on a blocking user-facing extension UI prompt. */
|
|
568
|
+
export interface UIPromptStartEvent {
|
|
569
|
+
type: "ui_prompt_start";
|
|
570
|
+
reason: "ui_prompt";
|
|
571
|
+
kind: UIPromptKind;
|
|
572
|
+
title?: string;
|
|
573
|
+
}
|
|
574
|
+
/** Fired when Pi is no longer waiting on a blocking user-facing extension UI prompt. */
|
|
575
|
+
export interface UIPromptEndEvent {
|
|
576
|
+
type: "ui_prompt_end";
|
|
577
|
+
reason: "ui_prompt";
|
|
578
|
+
kind: UIPromptKind;
|
|
579
|
+
title?: string;
|
|
580
|
+
}
|
|
566
581
|
/** Fired at the start of each turn */
|
|
567
582
|
export interface TurnStartEvent {
|
|
568
583
|
type: "turn_start";
|
|
@@ -788,7 +803,7 @@ export declare function isToolCallEventType<TName extends string, TInput extends
|
|
|
788
803
|
input: TInput;
|
|
789
804
|
};
|
|
790
805
|
/** Union of all event types */
|
|
791
|
-
export type ExtensionEvent = ProjectTrustEvent | ResourcesDiscoverEvent | SessionEvent | ContextEvent | BeforeProviderRequestEvent | BeforeProviderHeadersEvent | AfterProviderResponseEvent | BeforeAgentStartEvent | AgentStartEvent | AgentEndEvent | AgentSettledEvent | TurnStartEvent | TurnEndEvent | MessageStartEvent | MessageUpdateEvent | MessageEndEvent | ToolExecutionStartEvent | ToolExecutionUpdateEvent | ToolExecutionEndEvent | ModelSelectEvent | ThinkingLevelSelectEvent | UserBashEvent | InputEvent | ToolCallEvent | ToolResultEvent;
|
|
806
|
+
export type ExtensionEvent = ProjectTrustEvent | ResourcesDiscoverEvent | SessionEvent | ContextEvent | BeforeProviderRequestEvent | BeforeProviderHeadersEvent | AfterProviderResponseEvent | BeforeAgentStartEvent | AgentStartEvent | AgentEndEvent | AgentSettledEvent | UIPromptStartEvent | UIPromptEndEvent | TurnStartEvent | TurnEndEvent | MessageStartEvent | MessageUpdateEvent | MessageEndEvent | ToolExecutionStartEvent | ToolExecutionUpdateEvent | ToolExecutionEndEvent | ModelSelectEvent | ThinkingLevelSelectEvent | UserBashEvent | InputEvent | ToolCallEvent | ToolResultEvent;
|
|
792
807
|
export interface ContextEventResult {
|
|
793
808
|
messages?: AgentMessage[];
|
|
794
809
|
}
|
|
@@ -894,6 +909,8 @@ export interface ExtensionAPI {
|
|
|
894
909
|
on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>): void;
|
|
895
910
|
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
|
896
911
|
on(event: "agent_settled", handler: ExtensionHandler<AgentSettledEvent>): void;
|
|
912
|
+
on(event: "ui_prompt_start", handler: ExtensionHandler<UIPromptStartEvent>): void;
|
|
913
|
+
on(event: "ui_prompt_end", handler: ExtensionHandler<UIPromptEndEvent>): void;
|
|
897
914
|
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
898
915
|
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
|
|
899
916
|
on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
|
|
@@ -28,6 +28,9 @@ export interface LlamaModelsResponse {
|
|
|
28
28
|
data: LlamaModelInfo[];
|
|
29
29
|
object?: string;
|
|
30
30
|
}
|
|
31
|
+
export interface LlamaServerProps {
|
|
32
|
+
models_autoload?: boolean;
|
|
33
|
+
}
|
|
31
34
|
export interface LlamaModelEvent {
|
|
32
35
|
model: string;
|
|
33
36
|
event: string;
|
|
@@ -50,6 +53,9 @@ export declare class LlamaClient {
|
|
|
50
53
|
reload?: boolean;
|
|
51
54
|
signal?: AbortSignal;
|
|
52
55
|
}): Promise<LlamaModelInfo[]>;
|
|
56
|
+
props(options?: {
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}): Promise<LlamaServerProps>;
|
|
53
59
|
load(model: string, signal?: AbortSignal): Promise<void>;
|
|
54
60
|
unload(model: string, signal?: AbortSignal): Promise<void>;
|
|
55
61
|
unloadAndWait(model: string, signal?: AbortSignal): Promise<void>;
|
|
@@ -150,6 +150,13 @@ export class LlamaClient {
|
|
|
150
150
|
throw new Error("Server is not running in llama.cpp router mode");
|
|
151
151
|
return data;
|
|
152
152
|
}
|
|
153
|
+
async props(options = {}) {
|
|
154
|
+
const payload = await this.request("/props", { signal: options.signal });
|
|
155
|
+
if (typeof payload !== "object" || payload === null)
|
|
156
|
+
return {};
|
|
157
|
+
const { models_autoload: modelsAutoload } = payload;
|
|
158
|
+
return typeof modelsAutoload === "boolean" ? { models_autoload: modelsAutoload } : {};
|
|
159
|
+
}
|
|
153
160
|
async load(model, signal) {
|
|
154
161
|
await this.request("/models/load", { method: "POST", body: JSON.stringify({ model }), signal });
|
|
155
162
|
}
|
|
@@ -4,6 +4,8 @@ export declare const LLAMA_PROVIDER_ID = "llama.cpp";
|
|
|
4
4
|
export declare const DEFAULT_LLAMA_SERVER_URL = "http://127.0.0.1:8080";
|
|
5
5
|
export interface LlamaProviderController {
|
|
6
6
|
provider: Provider<"openai-completions">;
|
|
7
|
-
setCatalog(models: readonly LlamaModelInfo[], serverUrl: string
|
|
7
|
+
setCatalog(models: readonly LlamaModelInfo[], serverUrl: string, options?: {
|
|
8
|
+
routerAutoload?: boolean;
|
|
9
|
+
}): void;
|
|
8
10
|
}
|
|
9
11
|
export declare function createLlamaProvider(): LlamaProviderController;
|
|
@@ -10,6 +10,22 @@ async function resolveServerUrl(ctx, credential) {
|
|
|
10
10
|
const configured = credentialServerUrl(credential) ?? (await ctx.env("LLAMA_BASE_URL"))?.trim();
|
|
11
11
|
return configured ? normalizeLlamaServerUrl(configured) : undefined;
|
|
12
12
|
}
|
|
13
|
+
function modelIsSelectable(model, routerAutoload) {
|
|
14
|
+
if (model.status.value === "loaded")
|
|
15
|
+
return true;
|
|
16
|
+
// Unloaded presets are routable only when llama.cpp router autoload can load them on first use.
|
|
17
|
+
return routerAutoload && model.status.value === "unloaded" && !model.status.failed && model.source === "preset";
|
|
18
|
+
}
|
|
19
|
+
async function routerAutoloadEnabled(client, catalog, signal) {
|
|
20
|
+
if (!catalog.some((model) => model.status.value === "unloaded" && model.source === "preset"))
|
|
21
|
+
return false;
|
|
22
|
+
try {
|
|
23
|
+
return (await client.props({ signal })).models_autoload === true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
13
29
|
function toPiModel(model, serverUrl) {
|
|
14
30
|
const reportedContextWindow = model.meta?.n_ctx ?? model.meta?.n_ctx_train;
|
|
15
31
|
const contextWindow = reportedContextWindow && reportedContextWindow > 0 ? reportedContextWindow : 128000;
|
|
@@ -36,8 +52,10 @@ function toPiModel(model, serverUrl) {
|
|
|
36
52
|
}
|
|
37
53
|
export function createLlamaProvider() {
|
|
38
54
|
let models = [];
|
|
39
|
-
const setCatalog = (catalog, serverUrl) => {
|
|
40
|
-
models = catalog
|
|
55
|
+
const setCatalog = (catalog, serverUrl, options = {}) => {
|
|
56
|
+
models = catalog
|
|
57
|
+
.filter((model) => modelIsSelectable(model, options.routerAutoload === true))
|
|
58
|
+
.map((model) => toPiModel(model, serverUrl));
|
|
41
59
|
};
|
|
42
60
|
const provider = {
|
|
43
61
|
id: LLAMA_PROVIDER_ID,
|
|
@@ -101,12 +119,16 @@ export function createLlamaProvider() {
|
|
|
101
119
|
const serverUrl = credentialServerUrl(context.credential);
|
|
102
120
|
if (!serverUrl)
|
|
103
121
|
return;
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
.filter((model) => model.status.value === "loaded")
|
|
107
|
-
.map((model) => toPiModel(model, serverUrl));
|
|
122
|
+
const client = new LlamaClient(serverUrl, context.credential.key);
|
|
123
|
+
const catalog = await client.list({ signal: context.signal });
|
|
108
124
|
if (context.signal.aborted)
|
|
109
125
|
return;
|
|
126
|
+
const routerAutoload = await routerAutoloadEnabled(client, catalog, context.signal);
|
|
127
|
+
if (context.signal.aborted)
|
|
128
|
+
return;
|
|
129
|
+
const refreshed = catalog
|
|
130
|
+
.filter((model) => modelIsSelectable(model, routerAutoload))
|
|
131
|
+
.map((model) => toPiModel(model, serverUrl));
|
|
110
132
|
await context.publish({
|
|
111
133
|
persist: { models: refreshed, checkedAt: Date.now() },
|
|
112
134
|
update: () => {
|
|
@@ -279,11 +279,11 @@ export function loadEntriesFromFile(filePath) {
|
|
|
279
279
|
if (!existsSync(resolvedFilePath))
|
|
280
280
|
return [];
|
|
281
281
|
const entries = [];
|
|
282
|
+
let pending = "";
|
|
282
283
|
const fd = openSync(resolvedFilePath, "r");
|
|
283
284
|
try {
|
|
284
285
|
const decoder = new StringDecoder("utf8");
|
|
285
286
|
const buffer = Buffer.allocUnsafe(SESSION_READ_BUFFER_SIZE);
|
|
286
|
-
let pending = "";
|
|
287
287
|
while (true) {
|
|
288
288
|
const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
|
|
289
289
|
if (bytesRead === 0)
|
|
@@ -308,13 +308,15 @@ export function loadEntriesFromFile(filePath) {
|
|
|
308
308
|
finally {
|
|
309
309
|
closeSync(fd);
|
|
310
310
|
}
|
|
311
|
-
// Validate session header
|
|
311
|
+
// Validate session header before repairing the file.
|
|
312
312
|
if (entries.length === 0)
|
|
313
313
|
return entries;
|
|
314
314
|
const header = entries[0];
|
|
315
315
|
if (header.type !== "session" || typeof header.id !== "string") {
|
|
316
316
|
return [];
|
|
317
317
|
}
|
|
318
|
+
if (pending)
|
|
319
|
+
appendFileSync(resolvedFilePath, "\n");
|
|
318
320
|
return entries;
|
|
319
321
|
}
|
|
320
322
|
/**
|