@code-yeongyu/senpi 2026.10.25-1 → 2026.10.25-2

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 (68) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/bundle/chunks/{chunk-BTNYLNWZ.js → chunk-2EFVQWXZ.js} +2 -2
  3. package/dist/bundle/chunks/{chunk-RHHBXIRN.js → chunk-4WZRVZNX.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-J5P2VL3Y.js → chunk-B6TPQDBW.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-VOBD6FQG.js → chunk-FHXR4ZAC.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-7IRLFYNM.js → chunk-G2UETS2R.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-URBJS24R.js → chunk-J2U5VDG7.js} +15 -15
  8. package/dist/bundle/chunks/{chunk-YDCRA5ES.js → chunk-JRXKLIEQ.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-3VOG6WFU.js → chunk-KJZAY5YH.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-EYBDBR7V.js → chunk-KUE4AQN6.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-FWXMMMCV.js → chunk-LYGAV6KU.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-SGDOZT6B.js → chunk-MXZPAUFB.js} +1 -1
  13. package/dist/bundle/chunks/chunk-O5RYI3IW.js +2 -0
  14. package/dist/bundle/chunks/{chunk-II22E3CV.js → chunk-OI4NMCKP.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-7UUOGZKR.js → chunk-RXKNOEQQ.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-QH2HSCMX.js → chunk-TTU2RNVX.js} +3 -3
  17. package/dist/bundle/chunks/{chunk-PXKF2RHD.js → chunk-UFJ2K7Q7.js} +1 -1
  18. package/dist/bundle/chunks/chunk-VNN2V74Y.js +5 -0
  19. package/dist/bundle/chunks/{chunk-BMYNMUVZ.js → chunk-W2BKZBYJ.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-RVYSJBZ7.js → chunk-WFXUCVIR.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-7WAJCESX.js → chunk-XBUSVKWC.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-EWOEVVQT.js → chunk-YN7ELQ43.js} +1 -1
  23. package/dist/bundle/chunks/{cli-main-TABBP55A.js → cli-main-SWKFMTZQ.js} +1 -1
  24. package/dist/bundle/chunks/{host-command-OQR4RDTM.js → host-command-BC6UWP33.js} +1 -1
  25. package/dist/bundle/chunks/{host-lifecycle-U36U7JTE.js → host-lifecycle-TN5NEGNF.js} +1 -1
  26. package/dist/bundle/chunks/host-lifecycle.js +3 -3
  27. package/dist/bundle/chunks/{host-session-command-IGB27ZPJ.js → host-session-command-MGVBLBRV.js} +1 -1
  28. package/dist/bundle/chunks/{host-socket-request-ZDJGSSZO.js → host-socket-request-AS2OCRLP.js} +1 -1
  29. package/dist/bundle/chunks/{interactive-mode-DGQNSXUA.js → interactive-mode-SCSROHD5.js} +1 -1
  30. package/dist/bundle/chunks/{multi-session-host-M7HVUMRH.js → multi-session-host-VTTY5MFT.js} +1 -1
  31. package/dist/bundle/chunks/{package-manager-cli-FSR5P6YL.js → package-manager-cli-DZFOC3MF.js} +1 -1
  32. package/dist/bundle/chunks/{role-M2MTVACJ.js → role-JVSBNKGE.js} +1 -1
  33. package/dist/bundle/chunks/{rpc-mode-SZAJNKKL.js → rpc-mode-TOXYD4XA.js} +1 -1
  34. package/dist/bundle/chunks/{rpc-monitor-source-OL5KU6NO.js → rpc-monitor-source-B5NQ4ZVH.js} +1 -1
  35. package/dist/bundle/chunks/{runtime-session-CLNODAYE.js → runtime-session-V5OO7ZRM.js} +1 -1
  36. package/dist/bundle/chunks/{schedule-command-XBW4EOG5.js → schedule-command-SLISTG4V.js} +1 -1
  37. package/dist/bundle/chunks/{session-control-endpoint-WEF3XTJR.js → session-control-endpoint-HFFMBALG.js} +1 -1
  38. package/dist/bundle/chunks/{session-picker-KQUFWDQC.js → session-picker-465FVXQG.js} +1 -1
  39. package/dist/bundle/chunks/session-worker.js +18 -18
  40. package/dist/bundle/cli.js +2 -2
  41. package/dist/bundle/host-supervisor.js +3 -3
  42. package/dist/bundle/index.js +1 -1
  43. package/dist/bundle/rpc-entry.js +1 -1
  44. package/dist/bundle/runtime-manifest.json +1 -1
  45. package/dist/core/agent-session.d.ts +1 -0
  46. package/dist/core/agent-session.js +28 -9
  47. package/dist/core/engine-turn-limit.d.ts +2 -0
  48. package/dist/core/engine-turn-limit.js +7 -1
  49. package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +5 -0
  50. package/dist/core/extensions/builtin/goal/stop-pause.js +2 -1
  51. package/dist/core/extensions/builtin/loop/session-hold.js +2 -1
  52. package/dist/core/lifecycle-runtime-facts.js +3 -0
  53. package/dist/core/lifecycle-session-binding.js +6 -2
  54. package/dist/core/run-resume-turn.d.ts +1 -0
  55. package/dist/core/run-resume-turn.js +4 -3
  56. package/dist/core/stop-resume-directive.d.ts +1 -1
  57. package/dist/core/stop-resume-directive.js +10 -1
  58. package/dist/modes/interactive/interactive-mode.js +1 -0
  59. package/dist/modes/interactive/pause-footer.d.ts +5 -0
  60. package/dist/modes/interactive/pause-footer.js +2 -0
  61. package/dist/modes/rpc/host-reservations.js +3 -2
  62. package/dist/modes/rpc/host-stalled-evidence.js +2 -1
  63. package/dist/modes/rpc/tool-child/group-ownership.js +4 -2
  64. package/dist/utils/process-identity.d.ts +7 -0
  65. package/dist/utils/process-identity.js +32 -5
  66. package/package.json +6 -6
  67. package/dist/bundle/chunks/chunk-7ASLQ243.js +0 -2
  68. package/dist/bundle/chunks/chunk-XUFUKDKJ.js +0 -5
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
3
- import"./chunks/chunk-PXKF2RHD.js";import{main}from"./chunks/chunk-URBJS24R.js";import"./chunks/chunk-SGDOZT6B.js";import"./chunks/chunk-VOBD6FQG.js";import{configureHttpDispatcher}from"./chunks/chunk-BTNYLNWZ.js";import"./chunks/chunk-DXJJP6RP.js";import"./chunks/chunk-GXY5JEI5.js";import"./chunks/chunk-YDCRA5ES.js";import"./chunks/chunk-GY5MR5KR.js";import"./chunks/chunk-EHQXLHRB.js";import"./chunks/chunk-XF3LJC3O.js";import"./chunks/chunk-CNPDUC4D.js";import"./chunks/chunk-CDRFFHLL.js";import"./chunks/chunk-7UUOGZKR.js";import"./chunks/chunk-Y5OTHMXO.js";import"./chunks/chunk-MLVIYDHK.js";import"./chunks/chunk-TIVW4P37.js";import"./chunks/chunk-H4QOEVS5.js";import"./chunks/chunk-3LNSLJ7N.js";import"./chunks/chunk-MSQCTH6V.js";import"./chunks/chunk-KLAX467B.js";import"./chunks/chunk-JFFS72FL.js";import"./chunks/chunk-ORXZXUMC.js";import"./chunks/chunk-GZVCRV7I.js";import"./chunks/chunk-SOMFLLPD.js";import"./chunks/chunk-MTQQ2VQH.js";import"./chunks/chunk-3VOG6WFU.js";import"./chunks/chunk-BMYNMUVZ.js";import"./chunks/chunk-QMRJYMNH.js";import"./chunks/chunk-FWXMMMCV.js";import"./chunks/chunk-4AOMNOGY.js";import"./chunks/chunk-KUPQO7MC.js";import"./chunks/chunk-U67ML6CQ.js";import"./chunks/chunk-26UBFF6N.js";import"./chunks/chunk-EWOEVVQT.js";import"./chunks/chunk-II22E3CV.js";import"./chunks/chunk-7IRLFYNM.js";import"./chunks/chunk-XUFUKDKJ.js";import"./chunks/chunk-RHHBXIRN.js";import"./chunks/chunk-U3VOXCGP.js";import"./chunks/chunk-RVYSJBZ7.js";import"./chunks/chunk-HB35RBP4.js";import"./chunks/chunk-Z6WGAK7W.js";import"./chunks/chunk-DLAY7CIU.js";import"./chunks/chunk-3BTN337S.js";import"./chunks/chunk-QXREUA5F.js";import"./chunks/chunk-RUGYFYH7.js";import"./chunks/chunk-OEFCY46F.js";import"./chunks/chunk-KRNXUTZJ.js";import"./chunks/chunk-TAV6QZFM.js";import"./chunks/chunk-PELNNWGO.js";import"./chunks/chunk-QH2HSCMX.js";import"./chunks/chunk-FDL4CF6F.js";import"./chunks/chunk-BWA4FVFW.js";import"./chunks/chunk-YU4AYH4C.js";import"./chunks/chunk-E6WYH3ZL.js";import"./chunks/chunk-COH5F7QV.js";import"./chunks/chunk-RFB5CBW7.js";import"./chunks/chunk-2VZJTDUX.js";import"./chunks/chunk-E4LBKOZG.js";import"./chunks/chunk-GVSBCJFI.js";import"./chunks/chunk-WASCMT6Y.js";import"./chunks/chunk-T36LJ6MW.js";import"./chunks/chunk-QGJLZ6BI.js";import"./chunks/chunk-WHPDEDCC.js";import"./chunks/chunk-AA2L2RHH.js";import"./chunks/chunk-HK46JMSC.js";import"./chunks/chunk-4P7WARQZ.js";import"./chunks/chunk-WDTJ6JKE.js";import"./chunks/chunk-5ROMKDLK.js";import"./chunks/chunk-3W47LTXQ.js";import"./chunks/chunk-6AOVUTUR.js";import"./chunks/chunk-45SZOLCU.js";import"./chunks/chunk-DMI55M2N.js";import"./chunks/chunk-PJ34QVL5.js";import"./chunks/chunk-BMX2U22F.js";import"./chunks/chunk-YU2P3CAR.js";import"./chunks/chunk-7WAJCESX.js";import"./chunks/chunk-ZOFZRJSL.js";import"./chunks/chunk-2SOVDETB.js";import"./chunks/chunk-V3CK2AVC.js";import"./chunks/chunk-VRDU425E.js";import"./chunks/chunk-ZGAB7WQA.js";import{APP_NAME,scrubBrandFromEnvironment}from"./chunks/chunk-7YO7VQZW.js";import"./chunks/chunk-ATQFN5HI.js";import"./chunks/chunk-MIENF2YK.js";import"./chunks/chunk-OXNWERO4.js";import"./chunks/chunk-QN2BHP3M.js";import"./chunks/chunk-NTK2MHJ5.js";import"./chunks/chunk-7ASLQ243.js";import"./chunks/chunk-IKOFCODM.js";process.title=`${APP_NAME}-rpc`;scrubBrandFromEnvironment();process.env.PI_CODING_AGENT="true";process.env.AI_AGENT=APP_NAME;process.emitWarning=(()=>{});configureHttpDispatcher();main(["--mode","rpc",...process.argv.slice(2)]);
3
+ import"./chunks/chunk-UFJ2K7Q7.js";import{main}from"./chunks/chunk-J2U5VDG7.js";import"./chunks/chunk-MXZPAUFB.js";import"./chunks/chunk-FHXR4ZAC.js";import{configureHttpDispatcher}from"./chunks/chunk-2EFVQWXZ.js";import"./chunks/chunk-DXJJP6RP.js";import"./chunks/chunk-GXY5JEI5.js";import"./chunks/chunk-JRXKLIEQ.js";import"./chunks/chunk-GY5MR5KR.js";import"./chunks/chunk-EHQXLHRB.js";import"./chunks/chunk-XF3LJC3O.js";import"./chunks/chunk-CNPDUC4D.js";import"./chunks/chunk-CDRFFHLL.js";import"./chunks/chunk-RXKNOEQQ.js";import"./chunks/chunk-Y5OTHMXO.js";import"./chunks/chunk-MLVIYDHK.js";import"./chunks/chunk-TIVW4P37.js";import"./chunks/chunk-H4QOEVS5.js";import"./chunks/chunk-3LNSLJ7N.js";import"./chunks/chunk-MSQCTH6V.js";import"./chunks/chunk-KLAX467B.js";import"./chunks/chunk-JFFS72FL.js";import"./chunks/chunk-ORXZXUMC.js";import"./chunks/chunk-GZVCRV7I.js";import"./chunks/chunk-SOMFLLPD.js";import"./chunks/chunk-MTQQ2VQH.js";import"./chunks/chunk-KJZAY5YH.js";import"./chunks/chunk-W2BKZBYJ.js";import"./chunks/chunk-QMRJYMNH.js";import"./chunks/chunk-LYGAV6KU.js";import"./chunks/chunk-4AOMNOGY.js";import"./chunks/chunk-KUPQO7MC.js";import"./chunks/chunk-U67ML6CQ.js";import"./chunks/chunk-26UBFF6N.js";import"./chunks/chunk-YN7ELQ43.js";import"./chunks/chunk-OI4NMCKP.js";import"./chunks/chunk-G2UETS2R.js";import"./chunks/chunk-VNN2V74Y.js";import"./chunks/chunk-4WZRVZNX.js";import"./chunks/chunk-U3VOXCGP.js";import"./chunks/chunk-WFXUCVIR.js";import"./chunks/chunk-HB35RBP4.js";import"./chunks/chunk-Z6WGAK7W.js";import"./chunks/chunk-DLAY7CIU.js";import"./chunks/chunk-3BTN337S.js";import"./chunks/chunk-QXREUA5F.js";import"./chunks/chunk-RUGYFYH7.js";import"./chunks/chunk-OEFCY46F.js";import"./chunks/chunk-KRNXUTZJ.js";import"./chunks/chunk-TAV6QZFM.js";import"./chunks/chunk-PELNNWGO.js";import"./chunks/chunk-TTU2RNVX.js";import"./chunks/chunk-FDL4CF6F.js";import"./chunks/chunk-BWA4FVFW.js";import"./chunks/chunk-YU4AYH4C.js";import"./chunks/chunk-E6WYH3ZL.js";import"./chunks/chunk-COH5F7QV.js";import"./chunks/chunk-RFB5CBW7.js";import"./chunks/chunk-2VZJTDUX.js";import"./chunks/chunk-E4LBKOZG.js";import"./chunks/chunk-GVSBCJFI.js";import"./chunks/chunk-WASCMT6Y.js";import"./chunks/chunk-T36LJ6MW.js";import"./chunks/chunk-QGJLZ6BI.js";import"./chunks/chunk-WHPDEDCC.js";import"./chunks/chunk-AA2L2RHH.js";import"./chunks/chunk-HK46JMSC.js";import"./chunks/chunk-4P7WARQZ.js";import"./chunks/chunk-WDTJ6JKE.js";import"./chunks/chunk-5ROMKDLK.js";import"./chunks/chunk-3W47LTXQ.js";import"./chunks/chunk-6AOVUTUR.js";import"./chunks/chunk-45SZOLCU.js";import"./chunks/chunk-DMI55M2N.js";import"./chunks/chunk-PJ34QVL5.js";import"./chunks/chunk-BMX2U22F.js";import"./chunks/chunk-YU2P3CAR.js";import"./chunks/chunk-XBUSVKWC.js";import"./chunks/chunk-ZOFZRJSL.js";import"./chunks/chunk-2SOVDETB.js";import"./chunks/chunk-V3CK2AVC.js";import"./chunks/chunk-VRDU425E.js";import"./chunks/chunk-ZGAB7WQA.js";import{APP_NAME,scrubBrandFromEnvironment}from"./chunks/chunk-7YO7VQZW.js";import"./chunks/chunk-ATQFN5HI.js";import"./chunks/chunk-MIENF2YK.js";import"./chunks/chunk-OXNWERO4.js";import"./chunks/chunk-QN2BHP3M.js";import"./chunks/chunk-NTK2MHJ5.js";import"./chunks/chunk-O5RYI3IW.js";import"./chunks/chunk-IKOFCODM.js";process.title=`${APP_NAME}-rpc`;scrubBrandFromEnvironment();process.env.PI_CODING_AGENT="true";process.env.AI_AGENT=APP_NAME;process.emitWarning=(()=>{});configureHttpDispatcher();main(["--mode","rpc",...process.argv.slice(2)]);
@@ -1 +1 @@
1
- {"buildId":"3d444238d96c5025","externals":["@earendil-works/chord","@earendil-works/pi-pty","@silvia-odwyer/photon-node","bufferutil","canvas","kerberos","supports-color","utf-8-validate"]}
1
+ {"buildId":"256057df5dd08be8","externals":["@earendil-works/chord","@earendil-works/pi-pty","@silvia-odwyer/photon-node","bufferutil","canvas","kerberos","supports-color","utf-8-validate"]}
@@ -1708,6 +1708,7 @@ export declare class AgentSession {
1708
1708
  noteAutomaticInput(text: string): boolean;
1709
1709
  private _stopAddedSince;
1710
1710
  private _stopHoldLive;
1711
+ private _resumeCause;
1711
1712
  get pauseView(): {
1712
1713
  pauseSeq: number;
1713
1714
  records: {
@@ -56,7 +56,7 @@ import { assertExpectedUserLeaf, buildEditedUserMessage, UserEditError, userText
56
56
  import { ENGINE_PAUSED_ENTRY_TYPE, enginePauseSinceLastTurn, settledRepetitiveTurnPause } from "./engine-paused.js";
57
57
  import { EngineTurnHold, engineTurnHoldHistory, engineWakeSource } from "./engine-turn-hold.js";
58
58
  import { EngineTurnInput } from "./engine-turn-input.js";
59
- import { ENGINE_TURN_LIMIT_ENTRY_TYPE, ENGINE_TURN_LIMIT_EVENT, ENGINE_TURN_START_ENTRY_TYPE, engineTurnHistory, engineTurnLimitNotice, engineTurnStop, isUserDirectedTurn, } from "./engine-turn-limit.js";
59
+ import { ENGINE_TURN_LIMIT_ENTRY_TYPE, ENGINE_TURN_LIMIT_EVENT, ENGINE_TURN_START_ENTRY_TYPE, engineTurnHistory, engineTurnLimitNotice, engineTurnStop, isUserDirectedTurn, resetsEngineTurnHistory, } from "./engine-turn-limit.js";
60
60
  import { EngineTurnRelease } from "./engine-turn-release.js";
61
61
  import { environmentContextMessageIfChanged, resolveEnvironmentContext, } from "./environment-context.js";
62
62
  import { areExperimentalFeaturesEnabled } from "./experimental.js";
@@ -5771,7 +5771,11 @@ export class AgentSession {
5771
5771
  });
5772
5772
  const deliveredNotices = this._engineTurnHold.noticeCount;
5773
5773
  const deliveredHeldInputs = this._heldInputs.pending().length;
5774
- if (this._stopHoldLive() || this._engineTurnHold.fences.length > 0 || deliveredHeldInputs > 0) {
5774
+ // Payload-free notices (including goals) revalidate through their producer after release.
5775
+ // An interruption must not add a directive-only turn when that work has already completed.
5776
+ const resumeHeldTurn = this._stopHoldLive() &&
5777
+ (this._resumeCause() === "stop" || deliveredNotices === 0 || this._engineTurnHold.messages().length > 0);
5778
+ if (resumeHeldTurn || this._engineTurnHold.fences.length > 0 || deliveredHeldInputs > 0) {
5775
5779
  const { turnStarted, settled } = await this._resumeAfterStop(onQueued);
5776
5780
  return {
5777
5781
  answer: { resumed: true, pauseSeq, turnStarted, deliveredNotices, deliveredHeldInputs },
@@ -5810,8 +5814,9 @@ export class AgentSession {
5810
5814
  * resume settlement delivered the turn.
5811
5815
  */
5812
5816
  async _resumeAfterStop(onQueued) {
5817
+ const resumeCause = this._resumeCause();
5813
5818
  const sources = this._engineTurnHold.releaseAll("resume_session");
5814
- const { turnId, delivered } = await this._startResumeTurn(onQueued);
5819
+ const { turnId, delivered } = await this._startResumeTurn(onQueued, resumeCause);
5815
5820
  const settled = (async () => {
5816
5821
  if (!(await delivered)) {
5817
5822
  this._engineTurnHold.rejectInput();
@@ -5833,7 +5838,7 @@ export class AgentSession {
5833
5838
  return this._resumeSettlement;
5834
5839
  }
5835
5840
  /** The resume turn both resume paths start; held input and notices are consumed only if no newer Stop aborted it. */
5836
- async _startResumeTurn(onQueued) {
5841
+ async _startResumeTurn(onQueued, resumeCause = "stop") {
5837
5842
  const settlement = Promise.withResolvers();
5838
5843
  this._resumeSettlement = settlement.promise;
5839
5844
  try {
@@ -5864,7 +5869,7 @@ export class AgentSession {
5864
5869
  }));
5865
5870
  // The hold is already released: include new or re-held input before sending, and omit withdrawals.
5866
5871
  }
5867
- const started = await this._startResumeTurnWith(generation, continues, heldInputs, onQueued);
5872
+ const started = await this._startResumeTurnWith(generation, continues, heldInputs, onQueued, resumeCause);
5868
5873
  started.delivered.then(() => settlement.resolve(), () => settlement.resolve());
5869
5874
  return started;
5870
5875
  }
@@ -5873,8 +5878,9 @@ export class AgentSession {
5873
5878
  throw error;
5874
5879
  }
5875
5880
  }
5876
- _startResumeTurnWith(generation, continues, heldInputs, onQueued) {
5881
+ _startResumeTurnWith(generation, continues, heldInputs, onQueued, resumeCause = "stop") {
5877
5882
  return startRunResumeTurn({
5883
+ resumeCause,
5878
5884
  heldInputs: () => heldInputs,
5879
5885
  continueInputs: () => [...continues],
5880
5886
  heldWakes: () => this._engineTurnHold.messages(),
@@ -5906,7 +5912,7 @@ export class AgentSession {
5906
5912
  return;
5907
5913
  this._releaseExtrasGeneration = this._userAbortGeneration;
5908
5914
  if (this._stopHoldLive())
5909
- this._releaseExtras.push({ role: "custom", ...stopResumeDirective(), timestamp: Date.now() });
5915
+ this._releaseExtras.push({ role: "custom", ...stopResumeDirective(this._resumeCause()), timestamp: Date.now() });
5910
5916
  const { messages, commands } = this._takeHeldInputs();
5911
5917
  this._releaseExtras.push(...messages);
5912
5918
  // A later capture that finds no commands keeps the run an earlier one (a question answer's) still owes.
@@ -6019,7 +6025,7 @@ export class AgentSession {
6019
6025
  });
6020
6026
  }
6021
6027
  noteAutomaticInput(text) {
6022
- const userHold = this._engineTurnHold.holds.some((hold) => hold.kind === "session_hold" || hold.kind === "inherited_hold");
6028
+ const userHold = this._engineTurnHold.holds.some((hold) => (hold.kind === "session_hold" && hold.key !== "interrupted-run") || hold.kind === "inherited_hold");
6023
6029
  if (!userHold)
6024
6030
  return false;
6025
6031
  const message = {
@@ -6039,6 +6045,13 @@ export class AgentSession {
6039
6045
  _stopHoldLive() {
6040
6046
  return this._engineTurnHold.holds.some((hold) => hold.kind === "session_hold");
6041
6047
  }
6048
+ _resumeCause() {
6049
+ const holds = this._engineTurnHold.holds;
6050
+ return holds.some((hold) => hold.kind === "session_hold" && hold.key === "interrupted-run") &&
6051
+ !holds.some((hold) => hold.kind === "session_hold" && hold.key !== "interrupted-run")
6052
+ ? "interrupted"
6053
+ : "stop";
6054
+ }
6042
6055
  get pauseView() {
6043
6056
  return {
6044
6057
  pauseSeq: this._engineTurnHold.currentPauseSeq,
@@ -6150,7 +6163,8 @@ export class AgentSession {
6150
6163
  }
6151
6164
  /** Keeps this process's engine-turn record: a user message clears it, a tool call marks the running engine turn. */
6152
6165
  _observeEngineTurnMessage(message) {
6153
- if (message.role === "user" || (message.role === "custom" && isUserDirectedTurn(message.customType))) {
6166
+ if (message.role === "user" ||
6167
+ (message.role === "custom" && resetsEngineTurnHistory(message.customType, message.details))) {
6154
6168
  this._engineTurnStarts = [];
6155
6169
  this._engineResumeWindow = false;
6156
6170
  this._engineTurnPauseRecorded = false;
@@ -6615,6 +6629,7 @@ export class AgentSession {
6615
6629
  /** A user Stop: hold automatic turns until human input or resume_session, then stop the running turn (senpi#3811). */
6616
6630
  async stop() {
6617
6631
  this._runHolds.stamp();
6632
+ this.refreshLifecycle();
6618
6633
  this._stopInterrupt = { pauseSeq: this._engineTurnHold.currentPauseSeq, at: new Date().toISOString() };
6619
6634
  try {
6620
6635
  await this.abort();
@@ -9211,6 +9226,10 @@ export class AgentSession {
9211
9226
  this._extensionErrorListener = bindings.onError;
9212
9227
  }
9213
9228
  this._applyExtensionBindings(this._extensionRunner);
9229
+ // Opening a session is not explicit Resume. Install the same hold used by Stop before
9230
+ // session_start producers run, so automatic work waits quietly in notice custody.
9231
+ if (this._lifecycleBinding.controller.requiresHumanContinuation)
9232
+ this._engineTurnHold.addHold("session_hold", { key: "interrupted-run" });
9214
9233
  this._engineTurnHold.start();
9215
9234
  this.syncPromptCacheSafeWaitEnv();
9216
9235
  await this._extensionRunner.emit({
@@ -27,6 +27,8 @@ export interface EngineTurnStop {
27
27
  export declare function engineTurnLimitNotice(stop: EngineTurnStop): string;
28
28
  /** A `.` manual continue or a continue-from-leaf is the user's own request, not an automatic turn. */
29
29
  export declare function isUserDirectedTurn(customType: string): boolean;
30
+ /** Recovering an interrupted run is not a new message that resets the unattended turn budget. */
31
+ export declare function resetsEngineTurnHistory(customType: string, details?: unknown): boolean;
30
32
  /** The session's engine turns since the user's last message, in order: when each started and whether its reply called a tool. */
31
33
  export interface EngineTurnHistory {
32
34
  readonly starts: EngineTurnStartRecord[];
@@ -23,9 +23,15 @@ export function isUserDirectedTurn(customType) {
23
23
  customType === CONTINUE_FROM_LEAF_CUSTOM_TYPE ||
24
24
  customType === STOP_RESUME_CUSTOM_TYPE);
25
25
  }
26
+ /** Recovering an interrupted run is not a new message that resets the unattended turn budget. */
27
+ export function resetsEngineTurnHistory(customType, details) {
28
+ return (isUserDirectedTurn(customType) &&
29
+ !(customType === STOP_RESUME_CUSTOM_TYPE &&
30
+ details?.resumeCause === "interrupted"));
31
+ }
26
32
  function isUserMessage(entry) {
27
33
  if (entry.type === "custom_message")
28
- return isUserDirectedTurn(entry.customType);
34
+ return resetsEngineTurnHistory(entry.customType, entry.details);
29
35
  return entry.type === "message" && entry.message.role === "user";
30
36
  }
31
37
  function isEngineTurnStart(entry) {
@@ -24,6 +24,11 @@ async function* closingQueryMessages(query) {
24
24
  }
25
25
  }
26
26
  export async function createAttemptMessages(input, auth) {
27
+ // Claude Code loads auto memory independently of settingSources. Keep its private
28
+ // notes out of every subscription session, after all auth and caller overrides.
29
+ auth.options.env = { ...auth.options.env, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
30
+ if (typeof auth.options.settings !== "string")
31
+ auth.options.settings = { ...auth.options.settings, autoMemoryEnabled: false };
27
32
  if (input.createAttempt)
28
33
  return recordingLimitWindows(retainedAttemptMessages(await input.createAttempt(auth)), auth);
29
34
  const query = input.query({ prompt: input.prompt, options: auth.options });
@@ -1,7 +1,8 @@
1
1
  export const GOAL_STOP_PAUSED_ENTRY = "goal-stop-paused";
2
2
  export const GOAL_STOP_RESUMED_ENTRY = "goal-stop-resumed";
3
3
  export function stopHoldLive(ctx) {
4
- return ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold") === true;
4
+ return (ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold" && hold.key !== "interrupted-run") ===
5
+ true);
5
6
  }
6
7
  export function markStopPaused(pi, goalId) {
7
8
  pi.appendEntry(GOAL_STOP_PAUSED_ENTRY, { goalId });
@@ -2,6 +2,7 @@ export function sessionHeld(ctx) {
2
2
  return ctx.getPauseState?.().holds.some((hold) => hold.kind !== "run_fence") === true;
3
3
  }
4
4
  export function userStopped(ctx) {
5
- return ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold") === true;
5
+ return (ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold" && hold.key !== "interrupted-run") ===
6
+ true);
6
7
  }
7
8
  //# sourceMappingURL=session-hold.js.map
@@ -35,6 +35,9 @@ export function readLifecycleRuntimeFacts(session) {
35
35
  });
36
36
  }
37
37
  const holds = session.holds.flatMap((hold) => {
38
+ // The interruption gate is a human wait, not a user Stop. Preserve unattended and real Stop precedence.
39
+ if (hold.kind === "session_hold" && hold.key === "interrupted-run")
40
+ return [];
38
41
  switch (hold.kind) {
39
42
  case "unattended":
40
43
  case "session_hold":
@@ -44,8 +44,12 @@ export class LifecycleSessionBinding {
44
44
  });
45
45
  // A crashed receiver may have durable custody but no transcript entry yet. Recovery owns that
46
46
  // delivery already; register it before session_start can let the gateway enqueue a second carrier.
47
- const deliveryEntries = session.sessionManager.reduceHistory(lifecycleDeliveryEntries);
47
+ const deliveries = session.sessionManager.reduceHistory(lifecycleDeliveryEntries);
48
48
  for (const record of this.controller.obligations.active) {
49
+ // Discover consumed work before session_start can queue another automatic turn. The later
50
+ // recovery pass still writes interruption/tool records, but must not be the first to close admission.
51
+ if (record.ticketId && (record.state === "consumed" || deliveries.has(record.ticketId)))
52
+ this.controller.ledger.interrupted = true;
49
53
  if (record.state !== "queued" || !record.payload || !record.ticketId)
50
54
  continue;
51
55
  const ticketId = record.ticketId;
@@ -53,7 +57,7 @@ export class LifecycleSessionBinding {
53
57
  const deliveryId = deliveryIdOf(message);
54
58
  if (deliveryId === undefined)
55
59
  continue;
56
- if (deliveryEntries.has(ticketId))
60
+ if (deliveries.has(ticketId))
57
61
  session.externalAdmission.observePersisted(message);
58
62
  else
59
63
  session.externalAdmission.restoreQueued(deliveryId, () =>
@@ -2,6 +2,7 @@ import type { ImageContent } from "@earendil-works/pi-ai";
2
2
  import type { CustomMessage } from "./messages.ts";
3
3
  type Message = Pick<CustomMessage, "customType" | "content" | "display" | "details">;
4
4
  export interface RunResumePorts {
5
+ readonly resumeCause?: "stop" | "interrupted";
5
6
  readonly heldInputs: () => readonly {
6
7
  readonly inputId: string;
7
8
  readonly text: string;
@@ -1,4 +1,4 @@
1
- import { STOP_RESUME_CUSTOM_TYPE, stopResumeDirective } from "./stop-resume-directive.js";
1
+ import { stopResumeDirective } from "./stop-resume-directive.js";
2
2
  function partsOf(content) {
3
3
  return typeof content === "string" ? [{ type: "text", text: content }] : [...content];
4
4
  }
@@ -13,17 +13,18 @@ export async function startRunResumeTurn(ports) {
13
13
  const inputs = ports.heldInputs();
14
14
  const continues = ports.continueInputs();
15
15
  const wakes = ports.heldWakes();
16
+ const directive = stopResumeDirective(ports.resumeCause);
16
17
  const content = [
17
18
  ...inputs.flatMap((input) => [
18
19
  { type: "text", text: `The user wrote while the run was paused:\n${input.text}` },
19
20
  ...(input.images ?? []),
20
21
  ]),
21
22
  ...wakes.flatMap((wake) => partsOf(wake.message.content)),
22
- ...partsOf(stopResumeDirective().content),
23
+ ...partsOf(directive.content),
23
24
  ];
24
25
  const sendSettled = Promise.withResolvers();
25
26
  const started = ports.turnStarted(sendSettled.promise);
26
- const sending = ports.send({ customType: STOP_RESUME_CUSTOM_TYPE, content, display: inputs.length > 0 });
27
+ const sending = ports.send({ ...directive, content, display: inputs.length > 0 });
27
28
  sending.then(sendSettled.resolve, sendSettled.resolve);
28
29
  const turnId = await started;
29
30
  const delivered = (async () => {
@@ -1,4 +1,4 @@
1
1
  import type { CustomMessage } from "./messages.ts";
2
2
  export declare const STOP_RESUME_CUSTOM_TYPE = "session-resumed";
3
- export declare function stopResumeDirective(): Pick<CustomMessage, "customType" | "content" | "display">;
3
+ export declare function stopResumeDirective(cause?: "stop" | "interrupted"): Pick<CustomMessage, "customType" | "content" | "display" | "details">;
4
4
  //# sourceMappingURL=stop-resume-directive.d.ts.map
@@ -2,7 +2,16 @@ export const STOP_RESUME_CUSTOM_TYPE = "session-resumed";
2
2
  const DIRECTIVE = "<system-reminder>The user stopped the previous turn and has now resumed. Tool calls that the Stop interrupted show " +
3
3
  "an aborted result; they were not re-run and their side effects may be partial. Check their effects before repeating " +
4
4
  "any of them, then continue the work.</system-reminder>";
5
- export function stopResumeDirective() {
5
+ export function stopResumeDirective(cause = "stop") {
6
+ if (cause === "interrupted")
7
+ return {
8
+ customType: STOP_RESUME_CUSTOM_TYPE,
9
+ content: "<system-reminder>The previous run was interrupted and has now resumed. Interrupted tool calls may have " +
10
+ "partial side effects and were not re-run. Check their effects before repeating any of them, then continue " +
11
+ "the work.</system-reminder>",
12
+ display: false,
13
+ details: { resumeCause: cause },
14
+ };
6
15
  return { customType: STOP_RESUME_CUSTOM_TYPE, content: DIRECTIVE, display: false };
7
16
  }
8
17
  //# sourceMappingURL=stop-resume-directive.js.map
@@ -2117,6 +2117,7 @@ export class InteractiveMode {
2117
2117
  this.showExtensionError(error);
2118
2118
  },
2119
2119
  });
2120
+ this.setExtensionStatus(PAUSE_STATUS_KEY, pauseFooterText(this.session));
2120
2121
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
2121
2122
  this.setupAutocompleteProvider();
2122
2123
  const extensionRunner = this.session.extensionRunner;
@@ -3,6 +3,11 @@ interface PauseView {
3
3
  readonly paused: boolean;
4
4
  readonly pausedNoticeCount: number;
5
5
  readonly runPaused: boolean;
6
+ readonly pauseView?: {
7
+ readonly records: readonly {
8
+ readonly key: string;
9
+ }[];
10
+ };
6
11
  }
7
12
  export declare function pauseFooterText(view: PauseView): string | undefined;
8
13
  export declare function resumeCommandTarget(view: PauseView): "resume_session" | "session_picker";
@@ -3,6 +3,8 @@ export function pauseFooterText(view) {
3
3
  if (!view.paused && !view.runPaused)
4
4
  return undefined;
5
5
  const held = view.pausedNoticeCount === 1 ? "1 update held" : `${view.pausedNoticeCount} updates held`;
6
+ if (view.pauseView?.records.some((record) => record.key === "interrupted-run"))
7
+ return `Interrupted · ${view.pausedNoticeCount > 0 ? `${held} · ` : ""}type to continue, /resume to resume`;
6
8
  if (view.runPaused)
7
9
  return `Run paused · ${held} · type to continue, /resume to resume the run`;
8
10
  return `Paused · ${held} · type to continue, /resume to resume`;
@@ -22,7 +22,7 @@
22
22
  import { createHash } from "node:crypto";
23
23
  import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
24
24
  import { join } from "node:path";
25
- import { processIsLive, processStartTimeMs, readProcessStartTime, sameProcessStartMs, } from "../../utils/process-identity.js";
25
+ import { processIsLive, processIsRunning, processStartTimeMs, readProcessStartTime, sameProcessStartMs, } from "../../utils/process-identity.js";
26
26
  import { createHostDaemonPaths, generationPaths, HOST_DAEMON_DIR_ENV, hostDaemonDirectoryPaths, } from "./host-daemon-paths.js";
27
27
  import { parseJson, readFileOrUndefined } from "./host-daemon-state.js";
28
28
  import { generationHeartbeatStopped } from "./host-stalled-evidence.js";
@@ -280,7 +280,8 @@ export async function claimOwnerIsLive(owner) {
280
280
  * different start time. `unproven`: the pid runs but no start time was recorded or none can be read.
281
281
  */
282
282
  export async function claimOwnerIdentity(owner) {
283
- if (!processIsLive(owner.pid))
283
+ // An exited, unreaped owner (state Z) makes no more writes: the claim is gone (senpi#4119).
284
+ if (!processIsRunning(owner.pid))
284
285
  return "gone";
285
286
  const recorded = owner.processStartTime === null ? undefined : processStartTimeMs(owner.processStartTime);
286
287
  if (recorded === undefined) {
@@ -132,7 +132,8 @@ export async function hostChildAlive(generation) {
132
132
  if (!processIsLive(pid))
133
133
  return false;
134
134
  const observed = await readProcessIdentity(pid, process.platform, 1_000, processIsLive, "UTC");
135
- if (!processIsLive(pid))
135
+ // `absent` includes an exited, unreaped child (state Z), read in the same async snapshot (senpi#4119).
136
+ if (observed.kind === "absent" || !processIsLive(pid))
136
137
  return false;
137
138
  const recordedMs = typeof record?.processStartTime === "string" ? processStartTimeMs(record.processStartTime) : undefined;
138
139
  const observedMs = observed.kind === "present" ? processStartTimeMs(observed.identity) : undefined;
@@ -7,7 +7,7 @@
7
7
  * own group. A `detached` entry is never swept; the command explicitly opted out of the child's lifetime.
8
8
  */
9
9
  import { execFile } from "node:child_process";
10
- import { processIsLive, processStartTimeMs, readProcessStartTime, sameProcessStartMs, } from "../../../utils/process-identity.js";
10
+ import { processIsRunning, processStartTimeMs, readProcessStartTime, sameProcessStartMs, } from "../../../utils/process-identity.js";
11
11
  export async function classifyGroup(group, ownGroup, probes) {
12
12
  if (group.lifetime === "detached")
13
13
  return "detached";
@@ -70,7 +70,9 @@ function groupHasMembers(pgid) {
70
70
  }
71
71
  export const defaultGroupProbes = {
72
72
  groupHasMembers,
73
- isAlive: processIsLive,
73
+ // An exited, unreaped leader (state Z) has no start identity left to compare, so it must read as exited:
74
+ // its orphaned members keep the group, and the group is swept (senpi#3020, senpi#4119).
75
+ isAlive: processIsRunning,
74
76
  startOf: (pid) => readProcessStartTime(pid),
75
77
  ownGroup: () => processGroupOf(process.pid),
76
78
  signalGroup: (pgid, signal) => {
@@ -48,6 +48,13 @@ timeZone?: "UTC"): Promise<ProcessIdentityResult>;
48
48
  export declare function stopValidatedPid(pidFile: DaemonPidFile, signal: NodeJS.Signals): Promise<void>;
49
49
  export declare function waitForGone(pidFile: DaemonPidFile, timeoutMs: number): Promise<boolean>;
50
50
  export declare function readProcessStartTime(pid: number, platform?: NodeJS.Platform, timeoutMs?: number): Promise<string | undefined>;
51
+ /**
52
+ * `processIsLive` minus processes that have exited but are not reaped yet (state Z): a SIGKILLed host
53
+ * stays a zombie until its parent's loop, or init, waits on it, and `kill(pid, 0)` still succeeds for it
54
+ * (senpi#4119). One `ps` snapshot decides; a probe that fails any other way reads as running, so a
55
+ * starved probe on a live process is never taken for a dead one (#1609).
56
+ */
57
+ export declare function processIsRunning(pid: number): boolean;
51
58
  export declare function processIsLive(pid: number): boolean;
52
59
  /**
53
60
  * Wait for a spawned pid's process identity.
@@ -1,4 +1,4 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, execFileSync } from "node:child_process";
2
2
  const WINDOWS_FILETIME_EPOCH = 116444736000000000n;
3
3
  export const PROCESS_START_TOLERANCE_MS = 3_000;
4
4
  /** FILETIME and C/localized lstart/ISO identities share one millisecond representation. */
@@ -80,7 +80,7 @@ export class ProcessIdentityUnreadableError extends Error {
80
80
  * probe is retried within a bounded budget and only then surfaces as
81
81
  * ProcessIdentityUnreadableError. It never leaks the raw probe error as a verdict.
82
82
  */
83
- export async function processMatchesPidFile(pidFile, readStartTime = readProcessStartTime, isLive = processIsLive, retry = {}) {
83
+ export async function processMatchesPidFile(pidFile, readStartTime = readProcessStartTime, isLive = processIsRunning, retry = {}) {
84
84
  // A record without an identity guard answers only the liveness half of the question: a pid
85
85
  // that is gone is gone, and a live one stays unknown so it is never claimed or signalled.
86
86
  if (pidFile.processStartTime === null) {
@@ -129,7 +129,9 @@ timeZone) {
129
129
  `$process = Get-CimInstance Win32_Process -Filter "ProcessId=${String(pid)}" -ErrorAction Stop; if ($null -eq $process) { Write-Output '__SENPI_ABSENT__'; exit 0 }; $process.CreationDate.ToFileTimeUtc().ToString("D", [Globalization.CultureInfo]::InvariantCulture)`,
130
130
  ],
131
131
  }
132
- : { executable: "ps", args: ["-o", "lstart=", "-p", String(pid)] };
132
+ : // State and start time from ONE snapshot: an exited, unreaped process (state Z) keeps its start
133
+ // time but runs nothing, so it has no identity to match (senpi#4119).
134
+ { executable: "ps", args: ["-o", "stat=,lstart=", "-p", String(pid)] };
133
135
  return new Promise((resolve) => {
134
136
  const effectiveTimeoutMs = timeoutMs ?? (platform === "win32" ? 1_000 : undefined);
135
137
  execFile(command.executable, command.args, {
@@ -151,9 +153,13 @@ timeZone) {
151
153
  resolve({ kind: "error", error });
152
154
  return;
153
155
  }
154
- const output = stdout.trim();
155
- if (output === "__SENPI_ABSENT__")
156
+ const snapshot = stdout.trim();
157
+ if (snapshot === "__SENPI_ABSENT__")
156
158
  return resolve({ kind: "absent" });
159
+ const state = platform === "win32" ? "" : (snapshot.split(/\s+/, 1)[0] ?? "");
160
+ if (state.startsWith("Z"))
161
+ return resolve({ kind: "absent" });
162
+ const output = platform === "win32" ? snapshot : snapshot.slice(state.length).trim();
157
163
  if (!output || (platform === "win32" && !/^\d+$/.test(output)))
158
164
  return resolve({ kind: "error", error: new Error("invalid process identity output") });
159
165
  resolve({ kind: "present", identity: timeZone && platform !== "win32" ? `${output} UTC` : output });
@@ -191,6 +197,27 @@ export async function readProcessStartTime(pid, platform = process.platform, tim
191
197
  return result.kind === "present" ? result.identity : undefined;
192
198
  });
193
199
  }
200
+ /**
201
+ * `processIsLive` minus processes that have exited but are not reaped yet (state Z): a SIGKILLed host
202
+ * stays a zombie until its parent's loop, or init, waits on it, and `kill(pid, 0)` still succeeds for it
203
+ * (senpi#4119). One `ps` snapshot decides; a probe that fails any other way reads as running, so a
204
+ * starved probe on a live process is never taken for a dead one (#1609).
205
+ */
206
+ export function processIsRunning(pid) {
207
+ if (!processIsLive(pid))
208
+ return false;
209
+ if (process.platform === "win32")
210
+ return true;
211
+ try {
212
+ const state = execFileSync("ps", ["-o", "stat=", "-p", String(pid)], { encoding: "utf8", timeout: 2_000 });
213
+ return !state.trim().startsWith("Z");
214
+ }
215
+ catch (error) {
216
+ if (error instanceof Error && "status" in error && error.status === 1)
217
+ return false;
218
+ return true;
219
+ }
220
+ }
194
221
  export function processIsLive(pid) {
195
222
  try {
196
223
  process.kill(pid, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.10.25-1",
3
+ "version": "2026.10.25-2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -71,9 +71,9 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@earendil-works/chord": "0.99.1",
74
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.25-1",
75
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.25-1",
76
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.25-1",
74
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.10.25-2",
75
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.25-2",
76
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.25-2",
77
77
  "@silvia-odwyer/photon-node": "0.3.4",
78
78
  "chalk": "6.0.0",
79
79
  "cross-spawn": "7.0.6",
@@ -92,8 +92,8 @@
92
92
  "yaml": "2.9.1",
93
93
  "@anthropic-ai/claude-agent-sdk": "0.3.306",
94
94
  "@anthropic-ai/sdk": "0.127.0",
95
- "@code-yeongyu/senpi-codemode": "2026.10.25-1",
96
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.25-1",
95
+ "@code-yeongyu/senpi-codemode": "2026.10.25-2",
96
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.25-2",
97
97
  "@modelcontextprotocol/sdk": "1.32.1",
98
98
  "@mozilla/readability": "0.6.0",
99
99
  "linkedom": "0.18.13",
@@ -1,2 +0,0 @@
1
- import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
2
- import{execFile}from"node:child_process";var WINDOWS_FILETIME_EPOCH=116444736000000000n,PROCESS_START_TOLERANCE_MS=3e3;function processStartTimeMs(identity){if(/^\d+$/.test(identity)){let parsed2=Number((BigInt(identity)-WINDOWS_FILETIME_EPOCH)/10000n);return Number.isFinite(parsed2)?parsed2:void 0}let localIdentity=identity.trim(),korean=/^(\d{4})\uB144\s+(\d{1,2})\uC6D4\s+(\d{1,2})\uC77C\s+[\uC6D4\uD654\uC218\uBAA9\uAE08\uD1A0\uC77C]\uC694\uC77C\s+(\d{1,2})\uC2DC\s+(\d{1,2})\uBD84\s+(\d{1,2})\uCD08$/.exec(localIdentity),japanese=/^[\u65E5\u6708\u706B\u6C34\u6728\u91D1\u571F](?:\u66DC\u65E5)?\s+(\d{1,2})\/\s*(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(localIdentity),parts=korean?korean.slice(1):japanese?[japanese[6],japanese[1],japanese[2],japanese[3],japanese[4],japanese[5]]:void 0;if(parts===void 0){let parsed2=Date.parse(identity);return Number.isFinite(parsed2)?parsed2:void 0}let[year=NaN,month=NaN,day=NaN,hour=NaN,minute=NaN,second=NaN]=parts.map(Number),parsed=new Date(year,month-1,day,hour,minute,second,0);return parsed.getFullYear()===year&&parsed.getMonth()===month-1&&parsed.getDate()===day&&parsed.getHours()===hour&&parsed.getMinutes()===minute&&parsed.getSeconds()===second?parsed.getTime():void 0}function sameProcessStartMs(recorded,observed){return recorded!==void 0&&observed!==void 0&&Number.isFinite(recorded)&&Number.isFinite(observed)&&Math.abs(recorded-observed)<=PROCESS_START_TOLERANCE_MS}function parseDaemonPidFile(text){let parsed;try{parsed=JSON.parse(text)}catch(error){if(error instanceof SyntaxError)return;throw error}let unguarded=parsed!==null&&isRecord(parsed)&&parsed.processStartTime===null;if(!(!isRecord(parsed)||typeof parsed.pid!="number"||!unguarded&&typeof parsed.processStartTime!="string")&&!(!Number.isInteger(parsed.pid)||parsed.pid<=0)){if(unguarded)return{pid:parsed.pid,processStartTime:null};if(!(typeof parsed.processStartTime!="string"||parsed.processStartTime.trim()===""))return{pid:parsed.pid,processStartTime:parsed.processStartTime}}}var ProcessIdentityUnreadableError=class extends Error{constructor(pid,attempts,cause){super(`process identity for live pid ${pid} stayed unreadable after ${attempts} probe attempt(s): ${cause instanceof Error?cause.message:String(cause)}`),this.name="ProcessIdentityUnreadableError",this.pid=pid,this.attempts=attempts,this.cause=cause}};async function processMatchesPidFile(pidFile,readStartTime=readProcessStartTime,isLive=processIsLive,retry={}){if(pidFile.processStartTime===null){if(!isLive(pidFile.pid))return!1;throw new ProcessIdentityUnreadableError(pidFile.pid,0,new Error("pidfile carries no process identity guard"))}let attempts=Math.max(1,retry.attempts??5),delayMs=retry.delayMs??200,lastError;for(let attempt=1;attempt<=attempts;attempt++)try{let current=await readStartTime(pidFile.pid);if(current!==void 0){let recorded=processStartTimeMs(pidFile.processStartTime),observed=processStartTimeMs(current);if(recorded!==void 0&&observed!==void 0)return sameProcessStartMs(recorded,observed)}if(!isLive(pidFile.pid))return!1;lastError=new Error("process identity probe returned no usable identity for a live process"),attempt<attempts&&await new Promise(resolve=>setTimeout(resolve,delayMs))}catch(error){if(!isLive(pidFile.pid))return!1;lastError=error,attempt<attempts&&await new Promise(resolve=>setTimeout(resolve,delayMs))}throw new ProcessIdentityUnreadableError(pidFile.pid,attempts,lastError)}async function readProcessIdentity(pid,platform=process.platform,timeoutMs,isLive=processIsLive,timeZone){let command=platform==="win32"?{executable:"powershell.exe",args:["-NoProfile","-NonInteractive","-Command",`$process = Get-CimInstance Win32_Process -Filter "ProcessId=${String(pid)}" -ErrorAction Stop; if ($null -eq $process) { Write-Output '__SENPI_ABSENT__'; exit 0 }; $process.CreationDate.ToFileTimeUtc().ToString("D", [Globalization.CultureInfo]::InvariantCulture)`]}:{executable:"ps",args:["-o","lstart=","-p",String(pid)]};return new Promise(resolve=>{let effectiveTimeoutMs=timeoutMs??(platform==="win32"?1e3:void 0);execFile(command.executable,command.args,{windowsHide:!0,...effectiveTimeoutMs===void 0?{}:{timeout:effectiveTimeoutMs},...platform==="win32"&&timeZone===void 0?{}:{env:{...process.env,LC_ALL:"C",LANG:"C",...timeZone?{TZ:timeZone}:{}}}},(error,stdout)=>{if(error){let code="code"in error?error.code:void 0;if(platform!=="win32"&&code===1||!isLive(pid)){resolve({kind:"absent"});return}resolve({kind:"error",error});return}let output=stdout.trim();if(output==="__SENPI_ABSENT__")return resolve({kind:"absent"});if(!output||platform==="win32"&&!/^\d+$/.test(output))return resolve({kind:"error",error:new Error("invalid process identity output")});resolve({kind:"present",identity:timeZone&&platform!=="win32"?`${output} UTC`:output})}).once("error",error=>resolve({kind:"error",error}))})}async function readProcessStartTime(pid,platform=process.platform,timeoutMs){return readProcessIdentity(pid,platform,timeoutMs).then(result=>{if(result.kind==="error")throw result.error;return result.kind==="present"?result.identity:void 0})}function processIsLive(pid){try{return process.kill(pid,0),!0}catch(error){if(isNodeErrorCode(error,"ESRCH"))return!1;if(isNodeErrorCode(error,"EPERM"))return!0;throw error}}async function waitForStartTime(pid,timeoutMs,readStartTime=readProcessStartTime,isLive=processIsLive){let deadline=Date.now()+timeoutMs;for(;Date.now()<=deadline;){let startTime;try{startTime=await readStartTime(pid)}catch{}if(startTime)return startTime;await delay(20)}if(!isLive(pid))throw new Error(`spawned daemon pid ${pid} had no process start time`)}function delay(ms){return new Promise(resolveDelay=>setTimeout(resolveDelay,ms))}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function isNodeErrorCode(error,code){return error instanceof Error&&"code"in error&&error.code===code}export{PROCESS_START_TOLERANCE_MS,processStartTimeMs,sameProcessStartMs,parseDaemonPidFile,ProcessIdentityUnreadableError,processMatchesPidFile,readProcessIdentity,readProcessStartTime,processIsLive,waitForStartTime};