@code-yeongyu/senpi 2026.8.21-3 → 2026.8.22
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 +12 -0
- package/dist/core/extensions/builtin/permission-system/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/permission-system/index.js +7 -11
- package/dist/core/extensions/builtin/permission-system/index.js.map +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +14 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +15 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.d.ts +21 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.js +36 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.d.ts +5 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +95 -30
- package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
|
@@ -31,6 +31,7 @@ import { buildMcpStateResult, buildNeutralHookResult, buildPiBashError, buildPiB
|
|
|
31
31
|
import { AgentClientMessageSchema, AgentConversationTurnStructureSchema, AgentRunRequestSchema, AgentServerMessageSchema, AgentStoreConflictErrorSchema, AgentStoreConflictResultSchema, AssistantMessageSchema, BackgroundShellSpawnResultSchema, CanvasDiagnosticsErrorSchema, CanvasDiagnosticsResultSchema, ClientHeartbeatSchema, ComputerUseErrorSchema, ComputerUseResultSchema, ConversationActionSchema, ConversationSearchErrorSchema, ConversationSearchResultSchema, ConversationStateStructureSchema, ConversationStepSchema, ConversationTurnStructureSchema, DeleteErrorSchema, DeleteRejectedSchema, DeleteResultSchema, DeleteSuccessSchema, DiagnosticsErrorSchema, DiagnosticsRejectedSchema, DiagnosticsResultSchema, DiagnosticsSuccessSchema, ExecClientControlMessageSchema, ExecClientHeartbeatSchema, ExecClientMessageSchema, ExecClientStreamCloseSchema, ExecClientThrowSchema, FetchErrorSchema, FetchResultSchema, ForceBackgroundShellResultSchema, ForceBackgroundShellStatus, ForceBackgroundSubagentResultSchema, ForceBackgroundSubagentStatus, GetBlobResultSchema, GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, GrepContentMatchSchema, GrepContentResultSchema, GrepCountResultSchema, GrepErrorSchema, GrepFileCountSchema, GrepFileMatchSchema, GrepFilesResultSchema, GrepResultSchema, GrepSuccessSchema, GrepUnionResultSchema, KvClientMessageSchema, ListMcpResourcesExecResultSchema, ListMcpResourcesSuccessSchema, LsDirectoryTreeNode_FileSchema, LsDirectoryTreeNodeSchema, LsErrorSchema, LsRejectedSchema, LsResultSchema, LsSuccessSchema, McpAllowlistPrecheckResultSchema, McpApprovedSchema, McpArgsSchema, McpErrorSchema, McpImageContentSchema, McpRejectedSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolCallSchema, McpToolDefinitionSchema, McpToolErrorSchema, McpToolNotFoundSchema, McpToolResultContentItemSchema, McpToolResultSchema, ModelDetailsSchema, ReadErrorSchema, ReadMcpResourceExecResultSchema, ReadMcpResourceNotFoundSchema, ReadRejectedSchema, ReadResultSchema, ReadSuccessSchema, RecordScreenFailureSchema, RecordScreenResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, ResumeActionSchema, SelectedContextSchema, SelectedImageSchema, SetBlobResultSchema, ShellAllowlistPrecheckResultSchema, ShellFailureSchema, ShellRejectedSchema, ShellResultSchema, ShellStreamExitSchema, ShellStreamSchema, ShellStreamStartSchema, ShellStreamStderrSchema, ShellStreamStdoutSchema, ShellSuccessSchema, SmartModeClassifierErrorSchema, SmartModeClassifierResultSchema, SubagentAwaitNotFoundSchema, SubagentAwaitResultSchema, SubagentErrorSchema, SubagentResultSchema, ToolCallSchema, UserMessageActionSchema, UserMessageSchema, WebFetchAllowlistPrecheckResultSchema, WriteErrorSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, WriteSuccessSchema, } from "./cursor-agent/gen/agent_pb.js";
|
|
32
32
|
import { composeShellCommand, omitUndefinedArgs, piLimit, piLsPath, piReadArgs, piTimeout, } from "./cursor-agent/pi-args.js";
|
|
33
33
|
import { buildRequestedModel } from "./cursor-agent/reasoning-params.js";
|
|
34
|
+
import { CursorRetryableStreamError, cursorStreamRetryDelayMs, shouldRetryCursorStream, waitForCursorStreamRetry, } from "./cursor-agent/stream-retry.js";
|
|
34
35
|
import { CURSOR_CONVERSATION_POISONED_MESSAGE, createConversationRotationStore, isZeroTokenResourceExhausted, resolveConversationRotationPersistPath, } from "./cursor-conversation-rotation.js";
|
|
35
36
|
import { keepUsableCursorTaskArgs } from "./cursor-task-args.js";
|
|
36
37
|
export const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
@@ -38,7 +39,7 @@ export const CURSOR_CLIENT_VERSION = "cli-2026.07.23-e383d2b";
|
|
|
38
39
|
const EXEC_HEARTBEAT_INTERVAL_MS = 3000;
|
|
39
40
|
/** Maximum inbound silence before a Cursor turn without turnEnded is failed. */
|
|
40
41
|
export const CURSOR_STREAM_HEALTH_FAIL_THRESHOLD_MS = 30_000;
|
|
41
|
-
/**
|
|
42
|
+
/** @deprecated Heartbeats and checkpoints count as inbound liveness without a separate deadline. */
|
|
42
43
|
export const CURSOR_STREAM_HEALTH_HEARTBEAT_ONLY_THRESHOLD_MS = CURSOR_STREAM_HEALTH_FAIL_THRESHOLD_MS * 3;
|
|
43
44
|
/** Maximum time allowed to drain exec handlers after turnEnded. */
|
|
44
45
|
export const CURSOR_TURN_END_DRAIN_TIMEOUT_MS = 5000;
|
|
@@ -264,10 +265,14 @@ export const stream = (model, context, options) => {
|
|
|
264
265
|
let baseConversationId;
|
|
265
266
|
let conversationId;
|
|
266
267
|
let usageState;
|
|
267
|
-
let
|
|
268
|
+
let retryAttempt = false;
|
|
268
269
|
let attempt = 0;
|
|
270
|
+
let streamRetries = 0;
|
|
271
|
+
let forceResumeAction = false;
|
|
272
|
+
let pinnedRequestedModel;
|
|
273
|
+
let pinnedModelDetails;
|
|
269
274
|
do {
|
|
270
|
-
|
|
275
|
+
retryAttempt = false;
|
|
271
276
|
attempt += 1;
|
|
272
277
|
h2Settled = false;
|
|
273
278
|
sawTurnEnded = false;
|
|
@@ -278,6 +283,7 @@ export const stream = (model, context, options) => {
|
|
|
278
283
|
resolveH2 = resolve;
|
|
279
284
|
rejectH2 = reject;
|
|
280
285
|
});
|
|
286
|
+
let attemptSawCheckpoint = false;
|
|
281
287
|
try {
|
|
282
288
|
const apiKey = options?.apiKey;
|
|
283
289
|
if (!apiKey) {
|
|
@@ -288,11 +294,16 @@ export const stream = (model, context, options) => {
|
|
|
288
294
|
const blobStore = conversationBlobStores.get(conversationId) ?? new Map();
|
|
289
295
|
conversationBlobStores.set(conversationId, blobStore);
|
|
290
296
|
const cachedState = conversationStateCache.get(conversationId);
|
|
291
|
-
const { requestBytes, conversationState } = await buildGrpcRequest(model, context, options, {
|
|
297
|
+
const { requestBytes, conversationState, requestedModel, modelDetails } = await buildGrpcRequest(model, context, options, {
|
|
292
298
|
conversationId,
|
|
293
299
|
blobStore,
|
|
294
300
|
conversationState: cachedState,
|
|
301
|
+
forceResumeAction,
|
|
302
|
+
pinnedRequestedModel,
|
|
303
|
+
pinnedModelDetails,
|
|
295
304
|
});
|
|
305
|
+
pinnedRequestedModel ??= requestedModel;
|
|
306
|
+
pinnedModelDetails ??= modelDetails;
|
|
296
307
|
conversationStateCache.set(conversationId, conversationState);
|
|
297
308
|
const requestContextTools = buildMcpToolDefinitions(context.tools);
|
|
298
309
|
const baseUrl = model.baseUrl || CURSOR_API_URL;
|
|
@@ -313,9 +324,28 @@ export const stream = (model, context, options) => {
|
|
|
313
324
|
"x-cursor-client-type": "cli",
|
|
314
325
|
"x-request-id": randomUUID(),
|
|
315
326
|
};
|
|
316
|
-
|
|
317
|
-
h2Client
|
|
318
|
-
|
|
327
|
+
const attemptH2Client = http2.connect(baseUrl);
|
|
328
|
+
h2Client = attemptH2Client;
|
|
329
|
+
attemptH2Client.on("error", (error) => {
|
|
330
|
+
if (h2Client !== attemptH2Client)
|
|
331
|
+
return;
|
|
332
|
+
const mapped = mapH2TransportError(error, baseUrl);
|
|
333
|
+
settleH2(sawTurnEnded
|
|
334
|
+
? mapped
|
|
335
|
+
: new CursorRetryableStreamError(mapped instanceof Error ? mapped.message : String(mapped), "transport", { cause: mapped }));
|
|
336
|
+
});
|
|
337
|
+
attemptH2Client.on("goaway", () => {
|
|
338
|
+
if (h2Client === attemptH2Client && !h2Settled && !sawTurnEnded) {
|
|
339
|
+
settleH2(new CursorRetryableStreamError("Cursor HTTP/2 session received GOAWAY", "transport"));
|
|
340
|
+
h2Request?.close();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
attemptH2Client.on("close", () => {
|
|
344
|
+
if (h2Client === attemptH2Client && !h2Settled && !sawTurnEnded && !endStreamError) {
|
|
345
|
+
settleH2(new CursorRetryableStreamError("Cursor HTTP/2 session closed", "transport"));
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
h2Request = attemptH2Client.request(requestHeaders);
|
|
319
349
|
if (attempt === 1) {
|
|
320
350
|
stream.push({ type: "start", partial: output });
|
|
321
351
|
}
|
|
@@ -350,12 +380,11 @@ export const stream = (model, context, options) => {
|
|
|
350
380
|
};
|
|
351
381
|
openBlockState = state;
|
|
352
382
|
const onConversationCheckpoint = (checkpoint) => {
|
|
383
|
+
attemptSawCheckpoint = true;
|
|
353
384
|
conversationStateCache.set(conversationId, checkpoint);
|
|
354
385
|
};
|
|
355
386
|
const healthFailThresholdMs = options?.streamHealthFailThresholdMs ?? CURSOR_STREAM_HEALTH_FAIL_THRESHOLD_MS;
|
|
356
|
-
const heartbeatOnlyThresholdMs = options?.streamHealthHeartbeatOnlyThresholdMs ?? CURSOR_STREAM_HEALTH_HEARTBEAT_ONLY_THRESHOLD_MS;
|
|
357
387
|
let lastInboundFrameAt = Date.now();
|
|
358
|
-
let lastMeaningfulFrameAt = lastInboundFrameAt;
|
|
359
388
|
let turnEndCompletionStarted = false;
|
|
360
389
|
const armStreamHealthTimer = () => {
|
|
361
390
|
if (streamHealthTimer)
|
|
@@ -363,18 +392,17 @@ export const stream = (model, context, options) => {
|
|
|
363
392
|
if (sawTurnEnded || h2Settled)
|
|
364
393
|
return;
|
|
365
394
|
const now = Date.now();
|
|
366
|
-
const deadline =
|
|
395
|
+
const deadline = lastInboundFrameAt + healthFailThresholdMs;
|
|
367
396
|
streamHealthTimer = setTimeout(() => {
|
|
368
397
|
streamHealthTimer = null;
|
|
369
398
|
if (sawTurnEnded || h2Settled)
|
|
370
399
|
return;
|
|
371
400
|
const stalledFor = Date.now() - lastInboundFrameAt;
|
|
372
|
-
|
|
373
|
-
if (stalledFor < healthFailThresholdMs && meaningfulStalledFor < heartbeatOnlyThresholdMs) {
|
|
401
|
+
if (stalledFor < healthFailThresholdMs) {
|
|
374
402
|
armStreamHealthTimer();
|
|
375
403
|
return;
|
|
376
404
|
}
|
|
377
|
-
settleH2(new
|
|
405
|
+
settleH2(new CursorRetryableStreamError("Cursor stream ended before turnEnded: inbound stream stalled", "stall"));
|
|
378
406
|
h2Request?.close();
|
|
379
407
|
}, Math.max(0, deadline - now));
|
|
380
408
|
};
|
|
@@ -426,11 +454,7 @@ export const stream = (model, context, options) => {
|
|
|
426
454
|
? serverMessage.message.value.message?.case
|
|
427
455
|
: undefined;
|
|
428
456
|
const isTurnEnded = interactionUpdateCase === "turnEnded";
|
|
429
|
-
const isLivenessOnly = interactionUpdateCase === "heartbeat" ||
|
|
430
|
-
serverMessage.message.case === "conversationCheckpointUpdate";
|
|
431
457
|
lastInboundFrameAt = Date.now();
|
|
432
|
-
if (!isLivenessOnly)
|
|
433
|
-
lastMeaningfulFrameAt = lastInboundFrameAt;
|
|
434
458
|
armStreamHealthTimer();
|
|
435
459
|
// Dispatch is fire-and-forget so the socket keeps draining
|
|
436
460
|
// while a handler runs, but the promise is tracked: `done`
|
|
@@ -474,10 +498,17 @@ export const stream = (model, context, options) => {
|
|
|
474
498
|
}
|
|
475
499
|
});
|
|
476
500
|
h2Request.on("end", () => {
|
|
501
|
+
if (!sawTurnEnded && !endStreamError) {
|
|
502
|
+
settleH2(new CursorRetryableStreamError("Cursor stream ended before turnEnded", "clean-end"));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
477
505
|
settleH2();
|
|
478
506
|
});
|
|
479
507
|
h2Request.on("error", (error) => {
|
|
480
|
-
|
|
508
|
+
const mapped = mapH2TransportError(error, baseUrl);
|
|
509
|
+
settleH2(sawTurnEnded
|
|
510
|
+
? mapped
|
|
511
|
+
: new CursorRetryableStreamError(mapped instanceof Error ? mapped.message : String(mapped), "transport", { cause: mapped }));
|
|
481
512
|
});
|
|
482
513
|
if (options?.signal) {
|
|
483
514
|
options.signal.addEventListener("abort", () => {
|
|
@@ -512,6 +543,39 @@ export const stream = (model, context, options) => {
|
|
|
512
543
|
// bound: do not wait forever a second time in the error path.
|
|
513
544
|
if (!turnEndDrainTimedOut)
|
|
514
545
|
await drainInFlightDispatches();
|
|
546
|
+
const shouldRetryStream = shouldRetryCursorStream({
|
|
547
|
+
error,
|
|
548
|
+
retries: streamRetries,
|
|
549
|
+
maxRetries: options?.streamStallMaxRetries ?? 10,
|
|
550
|
+
sawTurnEnded,
|
|
551
|
+
aborted: options?.signal?.aborted === true,
|
|
552
|
+
});
|
|
553
|
+
if (shouldRetryStream) {
|
|
554
|
+
if (openBlockState) {
|
|
555
|
+
// Resume responses continue from the server checkpoint. Close any
|
|
556
|
+
// locally open cards before resetting per-attempt bookkeeping; no
|
|
557
|
+
// speculative replay deduplication is attempted.
|
|
558
|
+
endCurrentTextBlock(output, stream, openBlockState);
|
|
559
|
+
endCurrentThinkingBlock(output, stream, openBlockState);
|
|
560
|
+
flushOpenToolCalls(output, stream, openBlockState);
|
|
561
|
+
}
|
|
562
|
+
forceResumeAction ||= attemptSawCheckpoint;
|
|
563
|
+
const retryDelayMs = cursorStreamRetryDelayMs({
|
|
564
|
+
attempt: streamRetries,
|
|
565
|
+
fixedDelayMs: options?.streamStallRetryDelayMs,
|
|
566
|
+
});
|
|
567
|
+
streamRetries += 1;
|
|
568
|
+
retryAttempt = true;
|
|
569
|
+
await waitForCursorStreamRetry(retryDelayMs, options?.signal);
|
|
570
|
+
if (options?.signal?.aborted) {
|
|
571
|
+
retryAttempt = false;
|
|
572
|
+
output.stopReason = "aborted";
|
|
573
|
+
output.errorMessage = "Request was aborted";
|
|
574
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
575
|
+
stream.end();
|
|
576
|
+
}
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
515
579
|
// A stream that dies mid-turn leaves blocks open. Closing them here
|
|
516
580
|
// settles their live cards and pairs the server-owned calls that
|
|
517
581
|
// nothing else answers — an unpaired call is stripped from every
|
|
@@ -560,14 +624,14 @@ export const stream = (model, context, options) => {
|
|
|
560
624
|
const blobs = conversationBlobStores.get(conversationId);
|
|
561
625
|
if (blobs)
|
|
562
626
|
conversationBlobStores.set(decision.wireId, blobs);
|
|
563
|
-
|
|
627
|
+
retryAttempt = true;
|
|
564
628
|
}
|
|
565
629
|
else {
|
|
566
630
|
message = CURSOR_CONVERSATION_POISONED_MESSAGE;
|
|
567
631
|
}
|
|
568
632
|
}
|
|
569
633
|
}
|
|
570
|
-
if (!
|
|
634
|
+
if (!retryAttempt) {
|
|
571
635
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
572
636
|
output.errorMessage = message;
|
|
573
637
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
@@ -584,7 +648,7 @@ export const stream = (model, context, options) => {
|
|
|
584
648
|
h2Request = null;
|
|
585
649
|
h2Client = null;
|
|
586
650
|
}
|
|
587
|
-
} while (
|
|
651
|
+
} while (retryAttempt);
|
|
588
652
|
})();
|
|
589
653
|
return stream;
|
|
590
654
|
};
|
|
@@ -3270,7 +3334,7 @@ async function buildGrpcRequest(model, context, options, state) {
|
|
|
3270
3334
|
}
|
|
3271
3335
|
}
|
|
3272
3336
|
const action = create(ConversationActionSchema, {
|
|
3273
|
-
action: userContent && (userText.trim().length > 0 || hasUserImages)
|
|
3337
|
+
action: !state.forceResumeAction && userContent && (userText.trim().length > 0 || hasUserImages)
|
|
3274
3338
|
? {
|
|
3275
3339
|
case: "userMessageAction",
|
|
3276
3340
|
value: create(UserMessageActionSchema, {
|
|
@@ -3319,15 +3383,16 @@ async function buildGrpcRequest(model, context, options, state) {
|
|
|
3319
3383
|
rootPromptMessagesJson,
|
|
3320
3384
|
turns,
|
|
3321
3385
|
});
|
|
3322
|
-
const requestedModel = buildRequestedModel(model, options?.thinkingSelection);
|
|
3386
|
+
const requestedModel = state.pinnedRequestedModel ?? buildRequestedModel(model, options?.thinkingSelection);
|
|
3323
3387
|
const wireModelId = requestedModel.modelId;
|
|
3324
3388
|
const cursorMaxMode = model.compat?.cursorMaxMode === true;
|
|
3325
|
-
const modelDetails =
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3389
|
+
const modelDetails = state.pinnedModelDetails ??
|
|
3390
|
+
create(ModelDetailsSchema, {
|
|
3391
|
+
modelId: wireModelId,
|
|
3392
|
+
displayModelId: model.id,
|
|
3393
|
+
displayName: model.name,
|
|
3394
|
+
...(cursorMaxMode ? { maxMode: true } : undefined),
|
|
3395
|
+
});
|
|
3331
3396
|
const runRequest = create(AgentRunRequestSchema, {
|
|
3332
3397
|
conversationState,
|
|
3333
3398
|
action,
|
|
@@ -3347,7 +3412,7 @@ async function buildGrpcRequest(model, context, options, state) {
|
|
|
3347
3412
|
bytes: requestBytes.length,
|
|
3348
3413
|
tools: context.tools?.length ?? 0,
|
|
3349
3414
|
});
|
|
3350
|
-
return { requestBytes, blobStore, conversationState };
|
|
3415
|
+
return { requestBytes, blobStore, conversationState, requestedModel, modelDetails };
|
|
3351
3416
|
}
|
|
3352
3417
|
function hasImages(content) {
|
|
3353
3418
|
return content.some((item) => item.type === "image");
|