@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/extensions/builtin/permission-system/index.d.ts.map +1 -1
  3. package/dist/core/extensions/builtin/permission-system/index.js +7 -11
  4. package/dist/core/extensions/builtin/permission-system/index.js.map +1 -1
  5. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +14 -0
  6. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  7. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +15 -1
  8. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  9. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.d.ts +21 -0
  10. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.d.ts.map +1 -0
  11. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.js +36 -0
  12. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.js.map +1 -0
  13. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.d.ts +5 -0
  14. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.d.ts.map +1 -1
  15. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.js.map +1 -1
  16. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +1 -1
  17. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  18. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +95 -30
  19. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  20. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  21. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  22. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  23. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  24. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  25. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  26. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  27. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  28. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  29. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  30. 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
- /** Maximum heartbeat/checkpoint-only silence before a Cursor turn is failed. */
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 retryPoisonedConversation = false;
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
- retryPoisonedConversation = false;
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
- h2Client = http2.connect(baseUrl);
317
- h2Client.on("error", (error) => settleH2(mapH2TransportError(error, baseUrl)));
318
- h2Request = h2Client.request(requestHeaders);
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 = Math.min(lastInboundFrameAt + healthFailThresholdMs, lastMeaningfulFrameAt + heartbeatOnlyThresholdMs);
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
- const meaningfulStalledFor = Date.now() - lastMeaningfulFrameAt;
373
- if (stalledFor < healthFailThresholdMs && meaningfulStalledFor < heartbeatOnlyThresholdMs) {
401
+ if (stalledFor < healthFailThresholdMs) {
374
402
  armStreamHealthTimer();
375
403
  return;
376
404
  }
377
- settleH2(new Error("Cursor stream ended before turnEnded: inbound stream stalled"));
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
- settleH2(mapH2TransportError(error, baseUrl));
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
- retryPoisonedConversation = true;
627
+ retryAttempt = true;
564
628
  }
565
629
  else {
566
630
  message = CURSOR_CONVERSATION_POISONED_MESSAGE;
567
631
  }
568
632
  }
569
633
  }
570
- if (!retryPoisonedConversation) {
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 (retryPoisonedConversation);
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 = create(ModelDetailsSchema, {
3326
- modelId: wireModelId,
3327
- displayModelId: model.id,
3328
- displayName: model.name,
3329
- ...(cursorMaxMode ? { maxMode: true } : undefined),
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");