jeopi-cli 16.4.4 → 16.5.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 +10 -0
- package/dist/cli.js +2896 -2895
- package/dist/types/config/settings-schema.d.ts +27 -6
- package/dist/types/session/blob-store.d.ts +10 -1
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/package.json +14 -14
- package/src/cli/config-cli.ts +23 -7
- package/src/config/settings-schema.ts +32 -7
- package/src/config/settings.ts +43 -2
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -0
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +2 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/sdk.ts +30 -3
- package/src/session/agent-session.ts +16 -1
- package/src/session/blob-store.ts +19 -2
- package/src/session/exit-diagnostics.ts +106 -0
package/src/sdk.ts
CHANGED
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
import { AgentSession } from "./session/agent-session";
|
|
108
108
|
import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
|
|
109
109
|
import type { AuthStorage } from "./session/auth-storage";
|
|
110
|
+
import { createInterruptedTurnAbortMessage } from "./session/exit-diagnostics";
|
|
110
111
|
import {
|
|
111
112
|
type CustomMessage,
|
|
112
113
|
convertToLlm,
|
|
@@ -1235,11 +1236,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1235
1236
|
}
|
|
1236
1237
|
const secretsEnabled = obfuscator?.hasSecrets() === true;
|
|
1237
1238
|
|
|
1238
|
-
//
|
|
1239
|
-
|
|
1239
|
+
// An abnormal process exit after a non-terminal message tail is durable
|
|
1240
|
+
// evidence that the old process can no longer finish that turn. Preserve the
|
|
1241
|
+
// partial transcript and append one terminal aborted assistant record before
|
|
1242
|
+
// rebuilding runtime context. The helper is idempotent once that record exists.
|
|
1243
|
+
let existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
|
|
1244
|
+
const interruptedTurnAbort = createInterruptedTurnAbortMessage(existingBranch);
|
|
1245
|
+
if (interruptedTurnAbort) {
|
|
1246
|
+
sessionManager.appendMessage(interruptedTurnAbort);
|
|
1247
|
+
existingBranch = logger.time("getRecoveredSessionBranch", () => sessionManager.getBranch());
|
|
1248
|
+
}
|
|
1249
|
+
let existingSession = logger.time("loadSessionContext", () =>
|
|
1240
1250
|
deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
|
|
1241
1251
|
);
|
|
1242
|
-
const existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
|
|
1243
1252
|
const hasExistingSession = existingBranch.length > 0;
|
|
1244
1253
|
const hasThinkingEntry = existingBranch.some(entry => entry.type === "thinking_level_change");
|
|
1245
1254
|
const hasServiceTierEntry = existingBranch.some(entry => entry.type === "service_tier_change");
|
|
@@ -2029,6 +2038,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2029
2038
|
}
|
|
2030
2039
|
}
|
|
2031
2040
|
|
|
2041
|
+
// A first-turn user tail has no assistant metadata to copy. Once startup
|
|
2042
|
+
// has selected its final model, use that model to terminate the
|
|
2043
|
+
// interrupted turn before the live agent consumes the restored context.
|
|
2044
|
+
if (model) {
|
|
2045
|
+
const selectedModelAbort = createInterruptedTurnAbortMessage(existingBranch, {
|
|
2046
|
+
api: model.api,
|
|
2047
|
+
provider: model.provider,
|
|
2048
|
+
model: model.id,
|
|
2049
|
+
});
|
|
2050
|
+
if (selectedModelAbort) {
|
|
2051
|
+
sessionManager.appendMessage(selectedModelAbort);
|
|
2052
|
+
existingBranch = logger.time("getRecoveredUserTailBranch", () => sessionManager.getBranch());
|
|
2053
|
+
existingSession = logger.time("loadRecoveredUserTailContext", () =>
|
|
2054
|
+
deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2032
2059
|
// Discover custom commands (TypeScript slash commands)
|
|
2033
2060
|
const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
|
|
2034
2061
|
? { commands: [], errors: [] }
|
|
@@ -331,6 +331,7 @@ import {
|
|
|
331
331
|
import { findCompactMode } from "./compact-modes";
|
|
332
332
|
import {
|
|
333
333
|
collectPendingToolCalls,
|
|
334
|
+
createInterruptedTurnAbortMessage,
|
|
334
335
|
SESSION_EXIT_CUSTOM_TYPE,
|
|
335
336
|
type SessionExitData,
|
|
336
337
|
summarizeToolArguments,
|
|
@@ -14555,7 +14556,7 @@ export class AgentSession {
|
|
|
14555
14556
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
14556
14557
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
14557
14558
|
|
|
14558
|
-
|
|
14559
|
+
let sessionContext = this.buildDisplaySessionContext();
|
|
14559
14560
|
const didReloadConversationChange =
|
|
14560
14561
|
previousSessionContext !== undefined &&
|
|
14561
14562
|
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
@@ -14613,6 +14614,20 @@ export class AgentSession {
|
|
|
14613
14614
|
}
|
|
14614
14615
|
}
|
|
14615
14616
|
|
|
14617
|
+
const model = this.model;
|
|
14618
|
+
if (model) {
|
|
14619
|
+
const interruptedTurnAbort = createInterruptedTurnAbortMessage(this.sessionManager.getBranch(), {
|
|
14620
|
+
api: model.api,
|
|
14621
|
+
provider: model.provider,
|
|
14622
|
+
model: model.id,
|
|
14623
|
+
});
|
|
14624
|
+
if (interruptedTurnAbort) {
|
|
14625
|
+
this.sessionManager.appendMessage(interruptedTurnAbort);
|
|
14626
|
+
sessionContext = this.buildDisplaySessionContext();
|
|
14627
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
14628
|
+
}
|
|
14629
|
+
}
|
|
14630
|
+
|
|
14616
14631
|
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
14617
14632
|
const hasServiceTierEntry = this.sessionManager
|
|
14618
14633
|
.getBranch()
|
|
@@ -5,6 +5,9 @@ import { isEnoent, logger } from "jeopi-utils";
|
|
|
5
5
|
|
|
6
6
|
const BLOB_PREFIX = "blob:sha256:";
|
|
7
7
|
|
|
8
|
+
/** Canonical blob suffix: a 64-char lowercase hex SHA-256 digest, nothing else. */
|
|
9
|
+
const BLOB_HASH_RE = /^[a-f0-9]{64}$/;
|
|
10
|
+
|
|
8
11
|
export interface BlobPutOptions {
|
|
9
12
|
/** Optional file extension for a sidecar hardlink/copy that OS openers can type-detect. */
|
|
10
13
|
extension?: string;
|
|
@@ -179,10 +182,24 @@ export function isBlobRef(data: string): boolean {
|
|
|
179
182
|
return data.startsWith(BLOB_PREFIX);
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Extract the SHA-256 hash from a blob reference string.
|
|
187
|
+
*
|
|
188
|
+
* Returns null when the string is not a blob ref, or when the suffix is not a
|
|
189
|
+
* canonical 64-char lowercase hex hash. Rejecting non-hash suffixes here is the
|
|
190
|
+
* single choke point that keeps every resolution path confined to the blob dir:
|
|
191
|
+
* `get`/`getSync`/`has` feed this value into `path.join(this.dir, hash)`, so an
|
|
192
|
+
* unvalidated `../` suffix in a crafted session file would otherwise escape the
|
|
193
|
+
* store and read arbitrary files.
|
|
194
|
+
*/
|
|
183
195
|
export function parseBlobRef(data: string): string | null {
|
|
184
196
|
if (!data.startsWith(BLOB_PREFIX)) return null;
|
|
185
|
-
|
|
197
|
+
const hash = data.slice(BLOB_PREFIX.length);
|
|
198
|
+
if (!BLOB_HASH_RE.test(hash)) {
|
|
199
|
+
logger.warn("Rejected malformed blob reference", { suffix: hash });
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return hash;
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
/** Identify provider transport image data URLs so persistence can externalize and restore them losslessly. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "jeopi-agent-core";
|
|
2
|
+
import type { AssistantMessage } from "jeopi-ai";
|
|
2
3
|
import type { SessionEntry } from "./session-entries";
|
|
3
4
|
|
|
4
5
|
export const TOOL_EXECUTION_START_CUSTOM_TYPE = "tool_execution_start";
|
|
@@ -53,11 +54,116 @@ interface ToolCallContent {
|
|
|
53
54
|
arguments?: unknown;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
export interface AssistantModelMetadata {
|
|
58
|
+
api: AssistantMessage["api"];
|
|
59
|
+
provider: string;
|
|
60
|
+
model: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
57
64
|
if (typeof value !== "object") return false;
|
|
58
65
|
return value !== null;
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
function isPendingToolCallDiagnostic(value: unknown): value is PendingToolCallDiagnostic {
|
|
69
|
+
if (!isObject(value) || typeof value.toolName !== "string") return false;
|
|
70
|
+
if ("toolCallId" in value && typeof value.toolCallId !== "string") return false;
|
|
71
|
+
if ("intent" in value && typeof value.intent !== "string") return false;
|
|
72
|
+
if ("assistantTimestamp" in value && typeof value.assistantTimestamp !== "number") return false;
|
|
73
|
+
if ("startedAt" in value && typeof value.startedAt !== "string") return false;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readSessionExit(entry: SessionEntry): SessionExitData | undefined {
|
|
78
|
+
if (entry.type !== "custom" || entry.customType !== SESSION_EXIT_CUSTOM_TYPE || !isObject(entry.data)) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const { reason, kind, recordedAt } = entry.data;
|
|
82
|
+
if (
|
|
83
|
+
typeof reason !== "string" ||
|
|
84
|
+
(kind !== "normal" && kind !== "signal" && kind !== "fatal" && kind !== "process_exit") ||
|
|
85
|
+
typeof recordedAt !== "string"
|
|
86
|
+
) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const rawPending = entry.data.pendingToolCalls;
|
|
90
|
+
return {
|
|
91
|
+
reason,
|
|
92
|
+
kind,
|
|
93
|
+
recordedAt,
|
|
94
|
+
pendingToolCalls:
|
|
95
|
+
Array.isArray(rawPending) && rawPending.every(isPendingToolCallDiagnostic) ? rawPending : undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* createInterruptedTurnAbortMessage returns a terminal assistant record when
|
|
101
|
+
* the latest persisted process exit follows a non-terminal conversation tail.
|
|
102
|
+
*/
|
|
103
|
+
export function createInterruptedTurnAbortMessage(
|
|
104
|
+
entries: readonly SessionEntry[],
|
|
105
|
+
fallbackModel?: AssistantModelMetadata,
|
|
106
|
+
): AssistantMessage | undefined {
|
|
107
|
+
let exitIndex = -1;
|
|
108
|
+
let exit: SessionExitData | undefined;
|
|
109
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
110
|
+
const candidate = readSessionExit(entries[index]!);
|
|
111
|
+
if (!candidate) continue;
|
|
112
|
+
exitIndex = index;
|
|
113
|
+
exit = candidate;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (!exit || (exit.kind === "normal" && !exit.pendingToolCalls?.length)) return undefined;
|
|
117
|
+
|
|
118
|
+
let tailIndex = -1;
|
|
119
|
+
let tail: AgentMessage | undefined;
|
|
120
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
121
|
+
const entry = entries[index]!;
|
|
122
|
+
if (entry.type !== "message") continue;
|
|
123
|
+
tailIndex = index;
|
|
124
|
+
tail = entry.message;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
if (!tail || tailIndex > exitIndex) return undefined;
|
|
128
|
+
if (tail.role === "assistant" && !tail.content.some(isToolCallContent)) return undefined;
|
|
129
|
+
|
|
130
|
+
let previousAssistant: AssistantMessage | undefined;
|
|
131
|
+
for (let index = tailIndex; index >= 0; index--) {
|
|
132
|
+
const entry = entries[index]!;
|
|
133
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
134
|
+
previousAssistant = entry.message;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
tail.role === "toolResult" &&
|
|
139
|
+
(previousAssistant?.stopReason === "error" || previousAssistant?.stopReason === "aborted")
|
|
140
|
+
) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const model = previousAssistant ?? fallbackModel;
|
|
144
|
+
if (!model) return undefined;
|
|
145
|
+
|
|
146
|
+
const recordedAt = Date.parse(exit.recordedAt);
|
|
147
|
+
return {
|
|
148
|
+
role: "assistant",
|
|
149
|
+
content: [],
|
|
150
|
+
api: model.api,
|
|
151
|
+
provider: model.provider,
|
|
152
|
+
model: model.model,
|
|
153
|
+
usage: {
|
|
154
|
+
input: 0,
|
|
155
|
+
output: 0,
|
|
156
|
+
cacheRead: 0,
|
|
157
|
+
cacheWrite: 0,
|
|
158
|
+
totalTokens: 0,
|
|
159
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
160
|
+
},
|
|
161
|
+
stopReason: "aborted",
|
|
162
|
+
errorMessage: "Previous jeopi process exited before completing the turn.",
|
|
163
|
+
timestamp: Number.isFinite(recordedAt) ? recordedAt : Date.now(),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
61
167
|
function isToolCallContent(value: unknown): value is ToolCallContent {
|
|
62
168
|
if (!isObject(value)) return false;
|
|
63
169
|
return value.type === "toolCall" && (typeof value.name === "string" || typeof value.id === "string");
|