gsd-pi 2.64.0-dev.1a85e85 → 2.64.0-dev.4ac9673

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 (84) hide show
  1. package/dist/headless.js +3 -1
  2. package/dist/resources/extensions/bg-shell/bg-shell-lifecycle.js +22 -7
  3. package/dist/resources/extensions/bg-shell/process-manager.js +6 -1
  4. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +24 -13
  5. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +1 -0
  6. package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +1 -0
  7. package/dist/resources/extensions/gsd/notification-overlay.js +13 -9
  8. package/dist/resources/extensions/gsd/notification-store.js +10 -5
  9. package/dist/web/standalone/.next/BUILD_ID +1 -1
  10. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  11. package/dist/web/standalone/.next/build-manifest.json +2 -2
  12. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  13. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  14. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.html +1 -1
  30. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  37. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  38. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  39. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  40. package/package.json +1 -1
  41. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  42. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  43. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +8 -0
  44. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  45. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
  46. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  47. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +36 -0
  48. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  49. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +9 -0
  50. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +33 -0
  51. package/packages/pi-tui/dist/__tests__/overlay-layout.test.d.ts +2 -0
  52. package/packages/pi-tui/dist/__tests__/overlay-layout.test.d.ts.map +1 -0
  53. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +66 -0
  54. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -0
  55. package/packages/pi-tui/dist/components/loader.d.ts +4 -2
  56. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  57. package/packages/pi-tui/dist/components/loader.js +27 -9
  58. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  59. package/packages/pi-tui/dist/components/text.d.ts.map +1 -1
  60. package/packages/pi-tui/dist/components/text.js +2 -0
  61. package/packages/pi-tui/dist/components/text.js.map +1 -1
  62. package/packages/pi-tui/dist/overlay-layout.d.ts.map +1 -1
  63. package/packages/pi-tui/dist/overlay-layout.js +12 -1
  64. package/packages/pi-tui/dist/overlay-layout.js.map +1 -1
  65. package/packages/pi-tui/dist/tui.d.ts +4 -0
  66. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  67. package/packages/pi-tui/dist/tui.js +35 -0
  68. package/packages/pi-tui/dist/tui.js.map +1 -1
  69. package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +82 -0
  70. package/packages/pi-tui/src/components/loader.ts +27 -10
  71. package/packages/pi-tui/src/components/text.ts +1 -0
  72. package/packages/pi-tui/src/overlay-layout.ts +13 -1
  73. package/packages/pi-tui/src/tui.ts +34 -0
  74. package/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +19 -7
  75. package/src/resources/extensions/bg-shell/process-manager.ts +8 -2
  76. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +25 -13
  77. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +1 -0
  78. package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +1 -0
  79. package/src/resources/extensions/gsd/notification-overlay.ts +14 -9
  80. package/src/resources/extensions/gsd/notification-store.ts +8 -3
  81. package/src/resources/extensions/gsd/tests/complete-slice-string-coercion.test.ts +36 -0
  82. package/src/resources/extensions/gsd/tests/notification-store.test.ts +34 -1
  83. /package/dist/web/standalone/.next/static/{ffabZXz8JdN3EzX9EKt-R → 1btalZ1AEGX9RBvxBqJlC}/_buildManifest.js +0 -0
  84. /package/dist/web/standalone/.next/static/{ffabZXz8JdN3EzX9EKt-R → 1btalZ1AEGX9RBvxBqJlC}/_ssgManifest.js +0 -0
@@ -16,6 +16,7 @@ import {
16
16
  import {
17
17
  processes,
18
18
  pendingAlerts,
19
+ pushAlert,
19
20
  cleanupAll,
20
21
  cleanupSessionProcesses,
21
22
  persistManifest,
@@ -37,19 +38,30 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
37
38
  }
38
39
  }
39
40
 
40
- // Clean up on session shutdown
41
- pi.on("session_shutdown", async () => {
42
- cleanupAll();
43
- });
44
-
45
41
  // Register signal handlers to clean up bg processes on unexpected exit (fixes #428)
46
42
  const signalCleanup = () => {
47
43
  cleanupAll();
44
+ // Also kill bash-tool spawned children that bg-shell doesn't track
45
+ try {
46
+ const { listDescendants } = require("@gsd/native") as typeof import("@gsd/native");
47
+ const descendants = listDescendants(process.pid);
48
+ for (const childPid of descendants) {
49
+ try { process.kill(childPid, "SIGKILL"); } catch {}
50
+ }
51
+ } catch {}
48
52
  };
49
53
  process.on("SIGTERM", signalCleanup);
50
54
  process.on("SIGINT", signalCleanup);
51
55
  process.on("beforeExit", signalCleanup);
52
56
 
57
+ // Clean up on session shutdown — remove signal handlers to prevent accumulation
58
+ pi.on("session_shutdown", async () => {
59
+ process.off("SIGTERM", signalCleanup);
60
+ process.off("SIGINT", signalCleanup);
61
+ process.off("beforeExit", signalCleanup);
62
+ cleanupAll();
63
+ });
64
+
53
65
  // ── Compaction Awareness: Survive Context Resets ───────────────
54
66
 
55
67
  /** Build a compact state summary of all alive processes for context re-injection */
@@ -65,7 +77,7 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
65
77
  return ` - id:${p.id} "${p.label}" [${p.processType}] status:${p.status} uptime:${formatUptime(Date.now() - p.startedAt)}${portInfo}${urlInfo}${errInfo}${groupInfo}`;
66
78
  }).join("\n");
67
79
 
68
- pendingAlerts.push(
80
+ pushAlert(null,
69
81
  `${reason} ${alive.length} background process(es) are still running:\n${processSummaries}\nUse bg_shell digest/output/kill with these IDs.`
70
82
  );
71
83
  }
@@ -150,7 +162,7 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
150
162
  ` - ${s.id}: ${s.label} (pid ${s.pid}, type: ${s.processType}${s.group ? `, group: ${s.group}` : ""})`
151
163
  ).join("\n");
152
164
 
153
- pendingAlerts.push(
165
+ pushAlert(null,
154
166
  `${surviving.length} background process(es) from previous session still running:\n${summary}\n Note: These processes are outside bg_shell's control. Kill them manually if needed.`
155
167
  );
156
168
  }
@@ -33,6 +33,8 @@ export const processes = new Map<string, BgProcess>();
33
33
  /** Pending alerts to inject into the next agent context */
34
34
  export let pendingAlerts: string[] = [];
35
35
 
36
+ const MAX_PENDING_ALERTS = 50;
37
+
36
38
  /** Replace the pendingAlerts array (used by the extension entry point) */
37
39
  export function setPendingAlerts(alerts: string[]): void {
38
40
  pendingAlerts = alerts;
@@ -58,8 +60,12 @@ export function addEvent(bg: BgProcess, event: Omit<ProcessEvent, "timestamp">):
58
60
  }
59
61
  }
60
62
 
61
- export function pushAlert(bg: BgProcess, message: string): void {
62
- pendingAlerts.push(`[bg:${bg.id} ${bg.label}] ${message}`);
63
+ export function pushAlert(bg: BgProcess | null, message: string): void {
64
+ const prefix = bg ? `[bg:${bg.id} ${bg.label}] ` : "";
65
+ pendingAlerts.push(`${prefix}${message}`);
66
+ if (pendingAlerts.length > MAX_PENDING_ALERTS) {
67
+ pendingAlerts.splice(0, pendingAlerts.length - MAX_PENDING_ALERTS);
68
+ }
63
69
  }
64
70
 
65
71
  export function getInfo(p: BgProcess): BgProcessInfo {
@@ -804,27 +804,39 @@ export function registerDbTools(pi: ExtensionAPI): void {
804
804
  return m ? [m[1].trim(), m[2].trim()] : [s.trim(), ""];
805
805
  };
806
806
  const coerced = { ...params };
807
- coerced.filesModified = (params.filesModified ?? []).map((f: any) => {
807
+ // Coerce simple string-array fields: LLMs sometimes pass a plain string
808
+ // instead of a single-element array (#3585).
809
+ const wrapArray = (v: any): any[] =>
810
+ v == null ? [] : Array.isArray(v) ? v : [v];
811
+ coerced.provides = wrapArray(params.provides);
812
+ coerced.keyFiles = wrapArray(params.keyFiles);
813
+ coerced.keyDecisions = wrapArray(params.keyDecisions);
814
+ coerced.patternsEstablished = wrapArray(params.patternsEstablished);
815
+ coerced.observabilitySurfaces = wrapArray(params.observabilitySurfaces);
816
+ coerced.requirementsSurfaced = wrapArray(params.requirementsSurfaced);
817
+ coerced.drillDownPaths = wrapArray(params.drillDownPaths);
818
+ coerced.affects = wrapArray(params.affects);
819
+ coerced.filesModified = wrapArray(params.filesModified).map((f: any) => {
808
820
  if (typeof f !== "string") return f;
809
821
  const [path, description] = splitPair(f);
810
822
  return { path, description };
811
823
  });
812
- coerced.requires = (params.requires ?? []).map((r: any) => {
824
+ coerced.requires = wrapArray(params.requires).map((r: any) => {
813
825
  if (typeof r !== "string") return r;
814
826
  const [slice, provides] = splitPair(r);
815
827
  return { slice, provides };
816
828
  });
817
- coerced.requirementsAdvanced = (params.requirementsAdvanced ?? []).map((r: any) => {
829
+ coerced.requirementsAdvanced = wrapArray(params.requirementsAdvanced).map((r: any) => {
818
830
  if (typeof r !== "string") return r;
819
831
  const [id, how] = splitPair(r);
820
832
  return { id, how };
821
833
  });
822
- coerced.requirementsValidated = (params.requirementsValidated ?? []).map((r: any) => {
834
+ coerced.requirementsValidated = wrapArray(params.requirementsValidated).map((r: any) => {
823
835
  if (typeof r !== "string") return r;
824
836
  const [id, proof] = splitPair(r);
825
837
  return { id, proof };
826
838
  });
827
- coerced.requirementsInvalidated = (params.requirementsInvalidated ?? []).map((r: any) => {
839
+ coerced.requirementsInvalidated = wrapArray(params.requirementsInvalidated).map((r: any) => {
828
840
  if (typeof r !== "string") return r;
829
841
  const [id, what] = splitPair(r);
830
842
  return { id, what };
@@ -884,14 +896,14 @@ export function registerDbTools(pi: ExtensionAPI): void {
884
896
  deviations: Type.Optional(Type.String({ description: "Deviations from the slice plan, or 'None.'" })),
885
897
  knownLimitations: Type.Optional(Type.String({ description: "Known limitations or gaps, or 'None.'" })),
886
898
  followUps: Type.Optional(Type.String({ description: "Follow-up work discovered during execution, or 'None.'" })),
887
- keyFiles: Type.Optional(Type.Array(Type.String(), { description: "Key files created or modified" })),
888
- keyDecisions: Type.Optional(Type.Array(Type.String(), { description: "Key decisions made during this slice" })),
889
- patternsEstablished: Type.Optional(Type.Array(Type.String(), { description: "Patterns established by this slice" })),
890
- observabilitySurfaces: Type.Optional(Type.Array(Type.String(), { description: "Observability surfaces added" })),
891
- provides: Type.Optional(Type.Array(Type.String(), { description: "What this slice provides to downstream slices" })),
892
- requirementsSurfaced: Type.Optional(Type.Array(Type.String(), { description: "New requirements surfaced" })),
893
- drillDownPaths: Type.Optional(Type.Array(Type.String(), { description: "Paths to task summaries for drill-down" })),
894
- affects: Type.Optional(Type.Array(Type.String(), { description: "Downstream slices affected" })),
899
+ keyFiles: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Key files created or modified" })),
900
+ keyDecisions: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Key decisions made during this slice" })),
901
+ patternsEstablished: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Patterns established by this slice" })),
902
+ observabilitySurfaces: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Observability surfaces added" })),
903
+ provides: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "What this slice provides to downstream slices" })),
904
+ requirementsSurfaced: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "New requirements surfaced" })),
905
+ drillDownPaths: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Paths to task summaries for drill-down" })),
906
+ affects: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Downstream slices affected" })),
895
907
  requirementsAdvanced: Type.Optional(Type.Array(
896
908
  Type.Union([
897
909
  Type.Object({
@@ -44,6 +44,7 @@ export function registerShortcuts(pi: ExtensionAPI): void {
44
44
  minWidth: 60,
45
45
  maxHeight: "88%",
46
46
  anchor: "center",
47
+ backdrop: true,
47
48
  },
48
49
  },
49
50
  );
@@ -105,6 +105,7 @@ export async function handleNotificationsCommand(
105
105
  minWidth: 60,
106
106
  maxHeight: "88%",
107
107
  anchor: "center",
108
+ backdrop: true,
108
109
  },
109
110
  },
110
111
  );
@@ -157,13 +157,18 @@ export class GSDNotificationOverlay {
157
157
  }
158
158
 
159
159
  const content = this.buildContentLines(width);
160
- const viewportHeight = Math.max(5, process.stdout.rows ? process.stdout.rows - 8 : 24);
161
- const chromeHeight = 2; // top + bottom border
162
- const visibleContentRows = Math.max(1, viewportHeight - chromeHeight);
160
+ const maxVisibleRows = Math.max(5, process.stdout.rows ? process.stdout.rows - 8 : 24) - 2;
161
+ const visibleContentRows = Math.min(content.length, maxVisibleRows);
163
162
  const maxScroll = Math.max(0, content.length - visibleContentRows);
164
163
  this.scrollOffset = Math.min(this.scrollOffset, maxScroll);
165
164
  const visibleContent = content.slice(this.scrollOffset, this.scrollOffset + visibleContentRows);
166
165
 
166
+ // Pad to consistent height so filter changes don't leave ghost artifacts
167
+ // (differential renderer can't clear old overlay positions)
168
+ while (visibleContent.length < maxVisibleRows) {
169
+ visibleContent.push("");
170
+ }
171
+
167
172
  const lines = this.wrapInBox(visibleContent, width);
168
173
 
169
174
  this.cachedWidth = width;
@@ -253,13 +258,13 @@ export class GSDNotificationOverlay {
253
258
  const time = th.fg("dim", formatTimestamp(entry.ts));
254
259
  const source = entry.source === "workflow-logger" ? th.fg("dim", " [engine]") : "";
255
260
 
256
- // First line: icon + timestamp + source
257
- const msgMaxWidth = contentWidth - 20;
258
- const msg = entry.message.length > msgMaxWidth
259
- ? entry.message.slice(0, msgMaxWidth - 1) + "…"
260
- : entry.message;
261
+ // Measure actual prefix width to truncate message accurately
262
+ const prefix = `${coloredIcon} ${time}${source} `;
263
+ const prefixWidth = visibleWidth(prefix);
264
+ const msgMaxWidth = Math.max(10, contentWidth - prefixWidth);
265
+ const msg = truncateToWidth(entry.message, msgMaxWidth, "…");
261
266
 
262
- lines.push(row(`${coloredIcon} ${time}${source} ${msg}`));
267
+ lines.push(row(`${prefix}${msg}`));
263
268
  }
264
269
 
265
270
  return lines;
@@ -275,14 +275,19 @@ function _withLock<T>(basePath: string, fn: () => T): T {
275
275
  }
276
276
  }
277
277
 
278
+ // Only run the mutation if we actually own the lock
279
+ const ownsLock = fd !== null;
278
280
  try {
279
- // Write our PID timestamp into the lock for stale detection
280
- if (fd !== null) {
281
+ if (ownsLock && fd !== null) {
282
+ // Write our PID timestamp into the lock for stale detection
281
283
  writeFileSync(lockPath, String(Date.now()), "utf-8");
282
284
  closeSync(fd);
283
285
  }
284
286
  return fn();
285
287
  } finally {
286
- try { unlinkSync(lockPath); } catch { /* best-effort cleanup */ }
288
+ // Only delete the lock if we created it never remove another process's lock
289
+ if (ownsLock) {
290
+ try { unlinkSync(lockPath); } catch { /* best-effort cleanup */ }
291
+ }
287
292
  }
288
293
  }
@@ -124,6 +124,42 @@ describe("verificationEvidence sentinel coercion (#3565)", () => {
124
124
  });
125
125
  });
126
126
 
127
+ // ─── wrapArray coercion unit tests (#3585) ──────────────────────────────
128
+
129
+ describe("wrapArray coercion for simple string-array fields (#3585)", () => {
130
+ /**
131
+ * The wrapArray coercion logic extracted from db-tools.ts sliceCompleteExecute.
132
+ * Duplicated here so we can unit-test it directly.
133
+ */
134
+ function wrapArray(v: any): any[] {
135
+ return v == null ? [] : Array.isArray(v) ? v : [v];
136
+ }
137
+
138
+ test("null returns empty array", () => {
139
+ assert.deepEqual(wrapArray(null), []);
140
+ });
141
+
142
+ test("undefined returns empty array", () => {
143
+ assert.deepEqual(wrapArray(undefined), []);
144
+ });
145
+
146
+ test("plain string wraps into single-element array", () => {
147
+ assert.deepEqual(
148
+ wrapArray("Validated Tech UI flows and Portal self-service flows"),
149
+ ["Validated Tech UI flows and Portal self-service flows"],
150
+ );
151
+ });
152
+
153
+ test("array passes through unchanged", () => {
154
+ const arr = ["item1", "item2"];
155
+ assert.deepEqual(wrapArray(arr), arr);
156
+ });
157
+
158
+ test("empty array passes through unchanged", () => {
159
+ assert.deepEqual(wrapArray([]), []);
160
+ });
161
+ });
162
+
127
163
  // ─── Handler integration with coerced params ─────────────────────────────
128
164
 
129
165
  describe("handleCompleteSlice with coerced string arrays (#3565)", () => {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { describe, test, beforeEach, afterEach } from "node:test";
4
4
  import assert from "node:assert/strict";
5
- import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
5
+ import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { tmpdir } from "node:os";
8
8
 
@@ -246,4 +246,37 @@ describe("notification-store", () => {
246
246
  assert.equal(getUnreadCount(), 0);
247
247
  assert.equal(getLineCount(), 0);
248
248
  });
249
+
250
+ test("markAllRead does not delete a foreign lock file", () => {
251
+ initNotificationStore(tmp);
252
+ appendNotification("msg1", "info");
253
+
254
+ // Simulate another process holding the lock
255
+ const lockPath = join(tmp, ".gsd", "notifications.lock");
256
+ writeFileSync(lockPath, String(Date.now()), "utf-8");
257
+
258
+ // markAllRead should still work (best-effort) but not delete the foreign lock
259
+ markAllRead();
260
+
261
+ assert.ok(existsSync(lockPath), "foreign lock file should not be deleted");
262
+
263
+ // Clean up the lock so afterEach doesn't leave artifacts
264
+ rmSync(lockPath, { force: true });
265
+ });
266
+
267
+ test("clearNotifications does not delete a foreign lock file", () => {
268
+ initNotificationStore(tmp);
269
+ appendNotification("msg1", "info");
270
+
271
+ // Simulate another process holding the lock
272
+ const lockPath = join(tmp, ".gsd", "notifications.lock");
273
+ writeFileSync(lockPath, String(Date.now()), "utf-8");
274
+
275
+ // clearNotifications should still work but not delete the foreign lock
276
+ clearNotifications();
277
+
278
+ assert.ok(existsSync(lockPath), "foreign lock file should not be deleted");
279
+
280
+ rmSync(lockPath, { force: true });
281
+ });
249
282
  });