@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.
- package/CHANGELOG.md +16 -0
- package/dist/bundle/chunks/{chunk-BTNYLNWZ.js → chunk-2EFVQWXZ.js} +2 -2
- package/dist/bundle/chunks/{chunk-RHHBXIRN.js → chunk-4WZRVZNX.js} +1 -1
- package/dist/bundle/chunks/{chunk-J5P2VL3Y.js → chunk-B6TPQDBW.js} +1 -1
- package/dist/bundle/chunks/{chunk-VOBD6FQG.js → chunk-FHXR4ZAC.js} +1 -1
- package/dist/bundle/chunks/{chunk-7IRLFYNM.js → chunk-G2UETS2R.js} +2 -2
- package/dist/bundle/chunks/{chunk-URBJS24R.js → chunk-J2U5VDG7.js} +15 -15
- package/dist/bundle/chunks/{chunk-YDCRA5ES.js → chunk-JRXKLIEQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-3VOG6WFU.js → chunk-KJZAY5YH.js} +1 -1
- package/dist/bundle/chunks/{chunk-EYBDBR7V.js → chunk-KUE4AQN6.js} +1 -1
- package/dist/bundle/chunks/{chunk-FWXMMMCV.js → chunk-LYGAV6KU.js} +1 -1
- package/dist/bundle/chunks/{chunk-SGDOZT6B.js → chunk-MXZPAUFB.js} +1 -1
- package/dist/bundle/chunks/chunk-O5RYI3IW.js +2 -0
- package/dist/bundle/chunks/{chunk-II22E3CV.js → chunk-OI4NMCKP.js} +1 -1
- package/dist/bundle/chunks/{chunk-7UUOGZKR.js → chunk-RXKNOEQQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-QH2HSCMX.js → chunk-TTU2RNVX.js} +3 -3
- package/dist/bundle/chunks/{chunk-PXKF2RHD.js → chunk-UFJ2K7Q7.js} +1 -1
- package/dist/bundle/chunks/chunk-VNN2V74Y.js +5 -0
- package/dist/bundle/chunks/{chunk-BMYNMUVZ.js → chunk-W2BKZBYJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-RVYSJBZ7.js → chunk-WFXUCVIR.js} +1 -1
- package/dist/bundle/chunks/{chunk-7WAJCESX.js → chunk-XBUSVKWC.js} +1 -1
- package/dist/bundle/chunks/{chunk-EWOEVVQT.js → chunk-YN7ELQ43.js} +1 -1
- package/dist/bundle/chunks/{cli-main-TABBP55A.js → cli-main-SWKFMTZQ.js} +1 -1
- package/dist/bundle/chunks/{host-command-OQR4RDTM.js → host-command-BC6UWP33.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-U36U7JTE.js → host-lifecycle-TN5NEGNF.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +3 -3
- package/dist/bundle/chunks/{host-session-command-IGB27ZPJ.js → host-session-command-MGVBLBRV.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-ZDJGSSZO.js → host-socket-request-AS2OCRLP.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-DGQNSXUA.js → interactive-mode-SCSROHD5.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-M7HVUMRH.js → multi-session-host-VTTY5MFT.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-FSR5P6YL.js → package-manager-cli-DZFOC3MF.js} +1 -1
- package/dist/bundle/chunks/{role-M2MTVACJ.js → role-JVSBNKGE.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-SZAJNKKL.js → rpc-mode-TOXYD4XA.js} +1 -1
- package/dist/bundle/chunks/{rpc-monitor-source-OL5KU6NO.js → rpc-monitor-source-B5NQ4ZVH.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-CLNODAYE.js → runtime-session-V5OO7ZRM.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-XBW4EOG5.js → schedule-command-SLISTG4V.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-WEF3XTJR.js → session-control-endpoint-HFFMBALG.js} +1 -1
- package/dist/bundle/chunks/{session-picker-KQUFWDQC.js → session-picker-465FVXQG.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +18 -18
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +3 -3
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.js +28 -9
- package/dist/core/engine-turn-limit.d.ts +2 -0
- package/dist/core/engine-turn-limit.js +7 -1
- package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +5 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +2 -1
- package/dist/core/extensions/builtin/loop/session-hold.js +2 -1
- package/dist/core/lifecycle-runtime-facts.js +3 -0
- package/dist/core/lifecycle-session-binding.js +6 -2
- package/dist/core/run-resume-turn.d.ts +1 -0
- package/dist/core/run-resume-turn.js +4 -3
- package/dist/core/stop-resume-directive.d.ts +1 -1
- package/dist/core/stop-resume-directive.js +10 -1
- package/dist/modes/interactive/interactive-mode.js +1 -0
- package/dist/modes/interactive/pause-footer.d.ts +5 -0
- package/dist/modes/interactive/pause-footer.js +2 -0
- package/dist/modes/rpc/host-reservations.js +3 -2
- package/dist/modes/rpc/host-stalled-evidence.js +2 -1
- package/dist/modes/rpc/tool-child/group-ownership.js +4 -2
- package/dist/utils/process-identity.d.ts +7 -0
- package/dist/utils/process-identity.js +32 -5
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-7ASLQ243.js +0 -2
- package/dist/bundle/chunks/chunk-XUFUKDKJ.js +0 -5
package/dist/bundle/rpc-entry.js
CHANGED
|
@@ -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-
|
|
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":"
|
|
1
|
+
{"buildId":"256057df5dd08be8","externals":["@earendil-works/chord","@earendil-works/pi-pty","@silvia-odwyer/photon-node","bufferutil","canvas","kerberos","supports-color","utf-8-validate"]}
|
|
@@ -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
|
-
|
|
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" ||
|
|
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
|
|
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") ===
|
|
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") ===
|
|
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
|
|
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 (
|
|
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 {
|
|
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(
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
-
:
|
|
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
|
|
155
|
-
if (
|
|
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-
|
|
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-
|
|
75
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.25-
|
|
76
|
-
"@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.10.25-
|
|
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-
|
|
96
|
-
"@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.10.25-
|
|
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};
|