gsd-pi 2.31.0 → 2.31.1-dev.ffe48ad

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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { clearLock } from "./crash-recovery.js";
8
+ import { releaseSessionLock } from "./session-lock.js";
8
9
  import { nativeHasChanges } from "./native-git-bridge.js";
9
10
 
10
11
  // ─── SIGTERM Handling ─────────────────────────────────────────────────────────
@@ -23,6 +24,7 @@ export function registerSigtermHandler(
23
24
  ): () => void {
24
25
  if (previousHandler) process.off("SIGTERM", previousHandler);
25
26
  const handler = () => {
27
+ releaseSessionLock(currentBasePath);
26
28
  clearLock(currentBasePath);
27
29
  process.exit(0);
28
30
  };
@@ -51,6 +51,9 @@ let _lockedPath: string | null = null;
51
51
  /** Our PID at lock acquisition time. */
52
52
  let _lockPid: number = 0;
53
53
 
54
+ /** Set to true when proper-lockfile fires onCompromised (mtime drift, sleep, etc.). */
55
+ let _lockCompromised: boolean = false;
56
+
54
57
  const LOCK_FILE = "auto.lock";
55
58
 
56
59
  function lockPath(basePath: string): string {
@@ -102,13 +105,22 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
102
105
 
103
106
  const release = lockfile.lockSync(gsdDir, {
104
107
  realpath: false,
105
- stale: 300_000, // 5 minutes — consider lock stale if holder hasn't updated
108
+ stale: 1_800_000, // 30 minutes — safe for laptop sleep / long event loop stalls
106
109
  update: 10_000, // Update lock mtime every 10s to prove liveness
110
+ onCompromised: () => {
111
+ // proper-lockfile detected mtime drift (system sleep, event loop stall, etc.).
112
+ // Default handler throws inside setTimeout — an uncaught exception that crashes
113
+ // or corrupts process state. Instead, set a flag so validateSessionLock() can
114
+ // detect the compromise gracefully on the next dispatch cycle.
115
+ _lockCompromised = true;
116
+ _releaseFunction = null;
117
+ },
107
118
  });
108
119
 
109
120
  _releaseFunction = release;
110
121
  _lockedPath = basePath;
111
122
  _lockPid = process.pid;
123
+ _lockCompromised = false;
112
124
 
113
125
  // Safety net: clean up lock dir on process exit if _releaseFunction
114
126
  // wasn't called (e.g., normal exit after clean completion) (#1245).
@@ -233,6 +245,11 @@ export function updateSessionLock(
233
245
  * This is called periodically during the dispatch loop.
234
246
  */
235
247
  export function validateSessionLock(basePath: string): boolean {
248
+ // Lock was compromised by proper-lockfile (mtime drift from sleep, stall, etc.)
249
+ if (_lockCompromised) {
250
+ return false;
251
+ }
252
+
236
253
  // If we have an OS-level lock, we're still the owner
237
254
  if (_releaseFunction && _lockedPath === basePath) {
238
255
  return true;
@@ -284,6 +301,7 @@ export function releaseSessionLock(basePath: string): void {
284
301
 
285
302
  _lockedPath = null;
286
303
  _lockPid = 0;
304
+ _lockCompromised = false;
287
305
  }
288
306
 
289
307
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.31.0",
3
+ "version": "2.31.1-dev.ffe48ad",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-coding-agent",
3
- "version": "2.31.0",
3
+ "version": "2.31.1",
4
4
  "description": "Coding agent CLI (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "piConfig": {
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.31.0",
3
+ "version": "2.31.1",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { clearLock } from "./crash-recovery.js";
8
+ import { releaseSessionLock } from "./session-lock.js";
8
9
  import { nativeHasChanges } from "./native-git-bridge.js";
9
10
 
10
11
  // ─── SIGTERM Handling ─────────────────────────────────────────────────────────
@@ -23,6 +24,7 @@ export function registerSigtermHandler(
23
24
  ): () => void {
24
25
  if (previousHandler) process.off("SIGTERM", previousHandler);
25
26
  const handler = () => {
27
+ releaseSessionLock(currentBasePath);
26
28
  clearLock(currentBasePath);
27
29
  process.exit(0);
28
30
  };
@@ -51,6 +51,9 @@ let _lockedPath: string | null = null;
51
51
  /** Our PID at lock acquisition time. */
52
52
  let _lockPid: number = 0;
53
53
 
54
+ /** Set to true when proper-lockfile fires onCompromised (mtime drift, sleep, etc.). */
55
+ let _lockCompromised: boolean = false;
56
+
54
57
  const LOCK_FILE = "auto.lock";
55
58
 
56
59
  function lockPath(basePath: string): string {
@@ -102,13 +105,22 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
102
105
 
103
106
  const release = lockfile.lockSync(gsdDir, {
104
107
  realpath: false,
105
- stale: 300_000, // 5 minutes — consider lock stale if holder hasn't updated
108
+ stale: 1_800_000, // 30 minutes — safe for laptop sleep / long event loop stalls
106
109
  update: 10_000, // Update lock mtime every 10s to prove liveness
110
+ onCompromised: () => {
111
+ // proper-lockfile detected mtime drift (system sleep, event loop stall, etc.).
112
+ // Default handler throws inside setTimeout — an uncaught exception that crashes
113
+ // or corrupts process state. Instead, set a flag so validateSessionLock() can
114
+ // detect the compromise gracefully on the next dispatch cycle.
115
+ _lockCompromised = true;
116
+ _releaseFunction = null;
117
+ },
107
118
  });
108
119
 
109
120
  _releaseFunction = release;
110
121
  _lockedPath = basePath;
111
122
  _lockPid = process.pid;
123
+ _lockCompromised = false;
112
124
 
113
125
  // Safety net: clean up lock dir on process exit if _releaseFunction
114
126
  // wasn't called (e.g., normal exit after clean completion) (#1245).
@@ -233,6 +245,11 @@ export function updateSessionLock(
233
245
  * This is called periodically during the dispatch loop.
234
246
  */
235
247
  export function validateSessionLock(basePath: string): boolean {
248
+ // Lock was compromised by proper-lockfile (mtime drift from sleep, stall, etc.)
249
+ if (_lockCompromised) {
250
+ return false;
251
+ }
252
+
236
253
  // If we have an OS-level lock, we're still the owner
237
254
  if (_releaseFunction && _lockedPath === basePath) {
238
255
  return true;
@@ -284,6 +301,7 @@ export function releaseSessionLock(basePath: string): void {
284
301
 
285
302
  _lockedPath = null;
286
303
  _lockPid = 0;
304
+ _lockCompromised = false;
287
305
  }
288
306
 
289
307
  /**