@opengsd/gsd-pi 1.10.0 → 1.11.0

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 (218) hide show
  1. package/README.md +10 -9
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -0
  4. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  5. package/dist/resources/extensions/gsd/auto-verification.js +25 -12
  6. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
  7. package/dist/resources/extensions/gsd/db/engine.js +8 -2
  8. package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
  9. package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
  10. package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  11. package/dist/resources/extensions/gsd/doctor-engine-checks.js +16 -4
  12. package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
  13. package/dist/resources/extensions/gsd/model-router.js +10 -5
  14. package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
  15. package/dist/resources/extensions/gsd/preferences-models.js +50 -10
  16. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  17. package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
  18. package/dist/resources/extensions/gsd/preferences.js +7 -0
  19. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  20. package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
  21. package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
  22. package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
  23. package/dist/resources/extensions/subagent/index.js +1 -1
  24. package/dist/startup-model-validation.d.ts +1 -1
  25. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.html +1 -1
  46. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  53. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  56. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  57. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  58. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  59. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  60. package/integrations/hermes/pyproject.toml +1 -1
  61. package/package.json +7 -6
  62. package/packages/cloud-mcp-gateway/package.json +2 -2
  63. package/packages/contracts/dist/rpc.d.ts +1 -1
  64. package/packages/contracts/dist/rpc.d.ts.map +1 -1
  65. package/packages/contracts/dist/rpc.js +1 -1
  66. package/packages/contracts/dist/rpc.js.map +1 -1
  67. package/packages/contracts/dist/rpc.test.js +1 -1
  68. package/packages/contracts/dist/rpc.test.js.map +1 -1
  69. package/packages/contracts/package.json +1 -1
  70. package/packages/daemon/package.json +4 -4
  71. package/packages/gsd-agent-core/package.json +5 -5
  72. package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
  73. package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
  74. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  75. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
  76. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  77. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  78. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
  79. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  80. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
  81. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
  82. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  83. package/packages/gsd-agent-modes/package.json +7 -7
  84. package/packages/gsd-cloud/README.md +12 -2
  85. package/packages/gsd-cloud/dist/background-cli.test.js +157 -80
  86. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -1
  87. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  88. package/packages/gsd-cloud/dist/cli.js +36 -12
  89. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  90. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +6 -1
  91. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  92. package/packages/gsd-cloud/dist/cloud-runtime.js +106 -24
  93. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  94. package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
  95. package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
  96. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
  97. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
  98. package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
  99. package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
  100. package/packages/gsd-cloud/dist/runtime-process.d.ts +5 -0
  101. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -1
  102. package/packages/gsd-cloud/dist/runtime-process.js +429 -61
  103. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -1
  104. package/packages/gsd-cloud/dist/runtime-process.test.js +493 -47
  105. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -1
  106. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
  107. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
  108. package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
  109. package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
  110. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
  111. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
  112. package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
  113. package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
  114. package/packages/gsd-cloud/package.json +2 -2
  115. package/packages/mcp-server/package.json +4 -4
  116. package/packages/native/package.json +1 -1
  117. package/packages/pi-agent-core/dist/types.d.ts +2 -2
  118. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  119. package/packages/pi-agent-core/dist/types.js.map +1 -1
  120. package/packages/pi-agent-core/package.json +1 -1
  121. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  122. package/packages/pi-ai/dist/models.generated.d.ts +113 -2
  123. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  124. package/packages/pi-ai/dist/models.generated.js +84 -9
  125. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  126. package/packages/pi-ai/dist/models.js +18 -6
  127. package/packages/pi-ai/dist/models.js.map +1 -1
  128. package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
  129. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  130. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
  131. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  132. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  133. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  134. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  135. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
  136. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  137. package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
  138. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  139. package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
  140. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  141. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  142. package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
  143. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  145. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  146. package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
  147. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  148. package/packages/pi-ai/dist/providers/simple-options.js +1 -1
  149. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  150. package/packages/pi-ai/dist/types.d.ts +9 -1
  151. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  152. package/packages/pi-ai/dist/types.js.map +1 -1
  153. package/packages/pi-ai/package.json +1 -1
  154. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
  156. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
  158. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
  160. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  162. package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
  163. package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
  164. package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
  165. package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
  166. package/packages/pi-coding-agent/package.json +7 -7
  167. package/packages/pi-tui/package.json +2 -2
  168. package/packages/rpc-client/package.json +2 -2
  169. package/pkg/dist/theme/theme.d.ts +1 -1
  170. package/pkg/dist/theme/theme.d.ts.map +1 -1
  171. package/pkg/dist/theme/theme.js +1 -0
  172. package/pkg/dist/theme/theme.js.map +1 -1
  173. package/pkg/package.json +1 -1
  174. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
  175. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  176. package/src/resources/extensions/gsd/auto-verification.ts +46 -12
  177. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
  178. package/src/resources/extensions/gsd/db/engine.ts +9 -1
  179. package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
  180. package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
  181. package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  182. package/src/resources/extensions/gsd/doctor-engine-checks.ts +15 -4
  183. package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
  184. package/src/resources/extensions/gsd/model-router.ts +10 -5
  185. package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
  186. package/src/resources/extensions/gsd/preferences-models.ts +54 -10
  187. package/src/resources/extensions/gsd/preferences-types.ts +10 -2
  188. package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
  189. package/src/resources/extensions/gsd/preferences.ts +13 -0
  190. package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  191. package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
  192. package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
  193. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +64 -0
  194. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
  195. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  197. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
  198. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
  199. package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
  200. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
  201. package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
  202. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
  203. package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
  204. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
  205. package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
  206. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
  207. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
  208. package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
  209. package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
  210. package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
  211. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
  212. package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
  213. package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
  214. package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
  215. package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
  216. package/src/resources/extensions/subagent/index.ts +1 -1
  217. /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
  218. /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_ssgManifest.js +0 -0
@@ -1,16 +1,22 @@
1
1
  // Project/App: Open GSD
2
2
  // File Purpose: Detached cloud runtime process lifecycle and status persistence.
3
- import { spawn } from "node:child_process";
4
- import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
5
- import { dirname, join } from "node:path";
3
+ import { execFileSync, spawn } from "node:child_process";
4
+ import { randomUUID } from "node:crypto";
5
+ import { chmodSync, closeSync, linkSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
6
+ import { dirname } from "node:path";
6
7
  import { CLOUD_RUNTIME_INITIAL_CONNECT_WINDOW_MS } from "./cloud-runtime.js";
8
+ import { canonicalConfigPath, runtimeArtifactPath } from "./runtime-artifacts.js";
7
9
  const PROCESS_STARTUP_GRACE_MS = 5_000;
8
10
  const FORCED_STOP_TIMEOUT_MS = 5_000;
9
11
  const STOP_GRACE_PERIOD_MS = 5_000;
10
12
  const STOP_POLL_INTERVAL_MS = 50;
13
+ const MALFORMED_START_LOCK_GRACE_MS = 5_000;
11
14
  export const BACKGROUND_RUNTIME_READY_TIMEOUT_MS = CLOUD_RUNTIME_INITIAL_CONNECT_WINDOW_MS + PROCESS_STARTUP_GRACE_MS;
15
+ const START_LOCK_TIMEOUT_MS = BACKGROUND_RUNTIME_READY_TIMEOUT_MS
16
+ + 2 * (STOP_GRACE_PERIOD_MS + FORCED_STOP_TIMEOUT_MS)
17
+ + STOP_POLL_INTERVAL_MS;
12
18
  export async function startBackgroundRuntime(opts) {
13
- await acquireRuntimeStartLock(opts.configPath);
19
+ const releaseStartLock = await acquireRuntimeStartLock(opts.configPath);
14
20
  try {
15
21
  await stopBackgroundRuntime(opts.configPath);
16
22
  const logFile = runtimeLogPath(opts.configPath);
@@ -37,14 +43,32 @@ export async function startBackgroundRuntime(opts) {
37
43
  throw new Error(`could not start the background runtime; see ${logFile}`);
38
44
  }
39
45
  const pid = child.pid;
40
- writeRuntimeState(opts.configPath, pid, opts.projectDirs);
46
+ let processStartIdentity = null;
47
+ try {
48
+ processStartIdentity = (opts.processIdentityReader ?? readProcessStartIdentity)(pid);
49
+ if (!processStartIdentity) {
50
+ throw new Error(`could not determine process identity for PID ${pid}`);
51
+ }
52
+ writeRuntimeStateWithIdentity(opts.configPath, pid, opts.projectDirs, processStartIdentity);
53
+ }
54
+ catch (error) {
55
+ if (child.connected)
56
+ child.disconnect();
57
+ if (processStartIdentity) {
58
+ await terminateProcess(pid, processStartIdentity);
59
+ }
60
+ else {
61
+ await terminateKnownChild(child);
62
+ }
63
+ throw error;
64
+ }
41
65
  try {
42
66
  await waitUntilReady(child, opts.readyTimeoutMs ?? BACKGROUND_RUNTIME_READY_TIMEOUT_MS);
43
67
  }
44
68
  catch (error) {
45
69
  if (child.connected)
46
70
  child.disconnect();
47
- await terminateProcess(pid);
71
+ await terminateProcess(pid, processStartIdentity);
48
72
  removeRuntimeState(opts.configPath);
49
73
  throw error;
50
74
  }
@@ -59,7 +83,7 @@ export async function startBackgroundRuntime(opts) {
59
83
  };
60
84
  }
61
85
  finally {
62
- releaseRuntimeStartLock(opts.configPath);
86
+ releaseStartLock();
63
87
  }
64
88
  }
65
89
  /**
@@ -69,39 +93,55 @@ export async function startBackgroundRuntime(opts) {
69
93
  * same device token.
70
94
  */
71
95
  export function writeRuntimeState(configPath, pid, projects) {
96
+ const processStartIdentity = readProcessStartIdentity(pid);
97
+ if (!processStartIdentity) {
98
+ throw new Error(`could not determine process identity for PID ${pid}`);
99
+ }
100
+ writeRuntimeStateWithIdentity(configPath, pid, projects, processStartIdentity);
101
+ }
102
+ function writeRuntimeStateWithIdentity(configPath, pid, projects, processStartIdentity) {
72
103
  const statePath = runtimeStatePath(configPath);
73
104
  mkdirSync(dirname(statePath), { recursive: true });
74
- writePrivateJson(statePath, { pid, projects });
105
+ writePrivateJson(statePath, {
106
+ pid,
107
+ projects,
108
+ process_start_identity: processStartIdentity,
109
+ });
75
110
  }
76
111
  export function clearRuntimeState(configPath) {
77
112
  removeRuntimeState(configPath);
78
113
  }
79
114
  export async function stopBackgroundRuntime(configPath) {
80
- const state = readRuntimeState(configPath);
81
- if (!state)
115
+ const located = readRuntimeState(configPath);
116
+ if (!located)
82
117
  return false;
83
- await terminateProcess(state.pid);
84
- removeRuntimeState(configPath);
118
+ const processStartIdentity = located.state.process_start_identity;
119
+ if (!processStartIdentity || !runtimeProcessMatches(located.state)) {
120
+ removeRuntimeStateFile(located.path);
121
+ return false;
122
+ }
123
+ await terminateProcess(located.state.pid, processStartIdentity);
124
+ removeRuntimeStateFile(located.path);
85
125
  return true;
86
126
  }
87
127
  export function backgroundRuntimeStatus(configPath) {
88
- const state = readRuntimeState(configPath);
89
- if (!state) {
128
+ const located = readRuntimeState(configPath);
129
+ if (!located) {
90
130
  return { running: false, pid: null, projects: [], log_file: runtimeLogPath(configPath) };
91
131
  }
92
- if (!processIsRunning(state.pid)) {
93
- removeRuntimeState(configPath);
132
+ if (!runtimeProcessMatches(located.state)) {
133
+ removeRuntimeStateFile(located.path);
94
134
  return {
95
135
  running: false,
96
136
  pid: null,
97
- projects: state.projects,
137
+ projects: located.state.projects,
98
138
  log_file: runtimeLogPath(configPath),
99
139
  };
100
140
  }
101
141
  return {
102
142
  running: true,
103
- pid: state.pid,
104
- projects: state.projects,
143
+ pid: located.state.pid,
144
+ projects: located.state.projects,
105
145
  log_file: runtimeLogPath(configPath),
106
146
  };
107
147
  }
@@ -137,40 +177,45 @@ function waitUntilReady(child, timeoutMs) {
137
177
  child.on("message", onMessage);
138
178
  });
139
179
  }
140
- function runtimeStatePath(configPath) {
141
- return join(dirname(configPath), "cloud-runtime.json");
180
+ export function runtimeStatePath(configPath) {
181
+ return runtimeArtifactPath(configPath, "state");
142
182
  }
143
- function runtimeLogPath(configPath) {
144
- return join(dirname(configPath), "cloud-runtime.log");
183
+ export function runtimeLogPath(configPath) {
184
+ return runtimeArtifactPath(configPath, "log");
145
185
  }
146
186
  function runtimeStartLockPath(configPath) {
147
- return join(dirname(configPath), "cloud-runtime.start.lock");
187
+ return runtimeArtifactPath(configPath, "start.lock");
148
188
  }
149
- async function acquireRuntimeStartLock(configPath) {
189
+ export async function acquireRuntimeStartLock(configPath, onRecoveryClaimed) {
150
190
  const lockPath = runtimeStartLockPath(configPath);
151
191
  mkdirSync(dirname(lockPath), { recursive: true });
192
+ const processStartIdentity = readProcessStartIdentity(process.pid);
193
+ if (!processStartIdentity) {
194
+ throw new Error(`could not determine process identity for PID ${process.pid}`);
195
+ }
196
+ const owner = {
197
+ pid: process.pid,
198
+ process_start_identity: processStartIdentity,
199
+ };
152
200
  // Match worst-case `startBackgroundRuntime` hold time: stop prior runtime, wait for
153
201
  // ready, and tear down the child if ready fails, plus one poll interval.
154
- const deadline = Date.now()
155
- + BACKGROUND_RUNTIME_READY_TIMEOUT_MS
156
- + 2 * (STOP_GRACE_PERIOD_MS + FORCED_STOP_TIMEOUT_MS)
157
- + STOP_POLL_INTERVAL_MS;
202
+ const deadline = Date.now() + START_LOCK_TIMEOUT_MS;
158
203
  while (Date.now() < deadline) {
159
204
  try {
160
- const fd = openSync(lockPath, "wx");
161
- writeFileSync(fd, `${process.pid}\n`, "utf8");
162
- closeSync(fd);
163
- return;
205
+ createRuntimeStartLock(lockPath, owner);
206
+ let released = false;
207
+ return () => {
208
+ if (released)
209
+ return;
210
+ released = true;
211
+ releaseRuntimeStartLock(lockPath, owner);
212
+ };
164
213
  }
165
214
  catch (error) {
166
215
  if (error.code !== "EEXIST")
167
216
  throw error;
168
217
  try {
169
- const ownerPid = readRuntimeStartLockOwner(lockPath);
170
- const incompleteLockIsStale = ownerPid === null
171
- && Date.now() - statSync(lockPath).mtimeMs > 1_000;
172
- if ((ownerPid !== null && !processIsRunning(ownerPid)) || incompleteLockIsStale) {
173
- unlinkSync(lockPath);
218
+ if (recoverRuntimeStartLock(lockPath, configPath, onRecoveryClaimed)) {
174
219
  continue;
175
220
  }
176
221
  }
@@ -182,44 +227,329 @@ async function acquireRuntimeStartLock(configPath) {
182
227
  }
183
228
  throw new Error("timed out waiting for the background runtime start lock");
184
229
  }
185
- function releaseRuntimeStartLock(configPath) {
186
- const lockPath = runtimeStartLockPath(configPath);
187
- if (readRuntimeStartLockOwner(lockPath) !== process.pid)
188
- return;
230
+ function createRuntimeStartLock(lockPath, owner) {
231
+ const temporaryPath = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
189
232
  try {
190
- unlinkSync(lockPath);
233
+ writePrivateJson(temporaryPath, owner);
234
+ linkSync(temporaryPath, lockPath);
191
235
  }
192
- catch (error) {
193
- if (error.code !== "ENOENT")
194
- throw error;
236
+ finally {
237
+ removeRuntimeStateFile(temporaryPath);
195
238
  }
196
239
  }
240
+ function releaseRuntimeStartLock(lockPath, owner) {
241
+ if (!runtimeStartLockOwnersEqual(readRuntimeStartLockOwner(lockPath), owner))
242
+ return;
243
+ removeRuntimeStateFile(lockPath);
244
+ }
197
245
  function readRuntimeStartLockOwner(lockPath) {
198
246
  try {
199
- const pid = Number.parseInt(readFileSync(lockPath, "utf8").trim(), 10);
200
- return Number.isInteger(pid) && pid > 0 ? pid : null;
247
+ const text = readFileSync(lockPath, "utf8").trim();
248
+ if (/^\d+$/.test(text)) {
249
+ const pid = Number.parseInt(text, 10);
250
+ return pid > 0 ? { pid } : null;
251
+ }
252
+ const value = JSON.parse(text);
253
+ if (!Number.isInteger(value.pid) || (value.pid ?? 0) <= 0)
254
+ return null;
255
+ if (typeof value.process_start_identity !== "string")
256
+ return null;
257
+ return { pid: value.pid, process_start_identity: value.process_start_identity };
201
258
  }
202
259
  catch (error) {
203
260
  if (error.code === "ENOENT")
204
261
  return null;
262
+ return null;
263
+ }
264
+ }
265
+ function runtimeStartLockOwnerIsRunning(owner) {
266
+ if (!owner.process_start_identity)
267
+ return processIsRunning(owner.pid);
268
+ return processMatchesIdentity(owner.pid, owner.process_start_identity);
269
+ }
270
+ function runtimeStartLockCanBeRecovered(lockPath, configPath) {
271
+ const owner = readRuntimeStartLockOwner(lockPath);
272
+ if (owner?.process_start_identity)
273
+ return !runtimeStartLockOwnerIsRunning(owner);
274
+ const age = Date.now() - statSync(lockPath).mtimeMs;
275
+ if (owner) {
276
+ if (!runtimeStartLockOwnerIsRunning(owner))
277
+ return true;
278
+ return age > START_LOCK_TIMEOUT_MS
279
+ && inspectProcessCommandConfig(owner.pid, configPath) === false;
280
+ }
281
+ return age > MALFORMED_START_LOCK_GRACE_MS;
282
+ }
283
+ function recoverRuntimeStartLock(lockPath, configPath, onRecoveryClaimed) {
284
+ const recoveryPath = `${lockPath}.recovery.${process.pid}.${randomUUID()}`;
285
+ try {
286
+ linkSync(lockPath, recoveryPath);
287
+ }
288
+ catch (error) {
289
+ if (error.code === "ENOENT")
290
+ return false;
205
291
  throw error;
206
292
  }
293
+ try {
294
+ if (!runtimeStartLockCanBeRecovered(recoveryPath, configPath))
295
+ return false;
296
+ onRecoveryClaimed?.();
297
+ if (!pathsReferenceSameFile(lockPath, recoveryPath))
298
+ return false;
299
+ unlinkSync(lockPath);
300
+ return true;
301
+ }
302
+ finally {
303
+ removeRuntimeStateFile(recoveryPath);
304
+ }
305
+ }
306
+ function pathsReferenceSameFile(firstPath, secondPath) {
307
+ try {
308
+ const first = statSync(firstPath, { bigint: true });
309
+ const second = statSync(secondPath, { bigint: true });
310
+ return first.dev === second.dev && first.ino === second.ino;
311
+ }
312
+ catch {
313
+ return false;
314
+ }
315
+ }
316
+ function runtimeStartLockOwnersEqual(first, second) {
317
+ return first?.pid === second.pid
318
+ && first.process_start_identity === second.process_start_identity;
207
319
  }
208
320
  function readRuntimeState(configPath) {
321
+ const statePath = runtimeStatePath(configPath);
322
+ const current = readRuntimeStateFile(statePath);
323
+ if (current) {
324
+ if (typeof current.process_start_identity === "string"
325
+ && statePath !== legacyRuntimeStatePath(configPath)) {
326
+ return { path: statePath, state: current };
327
+ }
328
+ return migrateLegacyRuntimeState(configPath, statePath, current);
329
+ }
330
+ const legacyPath = legacyRuntimeStatePath(configPath);
331
+ if (legacyPath === statePath)
332
+ return null;
333
+ const legacy = readRuntimeStateFile(legacyPath);
334
+ if (!legacy)
335
+ return null;
336
+ return migrateLegacyRuntimeState(configPath, legacyPath, legacy);
337
+ }
338
+ function readRuntimeStateFile(path) {
209
339
  try {
210
- const value = JSON.parse(readFileSync(runtimeStatePath(configPath), "utf8"));
340
+ const value = JSON.parse(readFileSync(path, "utf8"));
211
341
  const pid = value.pid;
212
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || !Array.isArray(value.projects))
342
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0
343
+ || !Array.isArray(value.projects))
213
344
  return null;
214
345
  return {
215
346
  pid,
216
347
  projects: value.projects.filter((project) => typeof project === "string"),
348
+ process_start_identity: value.process_start_identity,
217
349
  };
218
350
  }
219
351
  catch {
220
352
  return null;
221
353
  }
222
354
  }
355
+ function migrateLegacyRuntimeState(configPath, sourcePath, state) {
356
+ const destinationPath = runtimeStatePath(configPath);
357
+ if (sourcePath === legacyRuntimeStatePath(configPath)
358
+ && !processCommandMatchesConfig(state.pid, configPath))
359
+ return null;
360
+ if (typeof state.process_start_identity === "string") {
361
+ if (sourcePath !== destinationPath) {
362
+ writeRuntimeStateWithIdentity(configPath, state.pid, state.projects, state.process_start_identity);
363
+ removeRuntimeStateFile(sourcePath);
364
+ }
365
+ return { path: destinationPath, state };
366
+ }
367
+ const processStartIdentity = readProcessStartIdentity(state.pid);
368
+ if (!processStartIdentity)
369
+ return null;
370
+ writeRuntimeStateWithIdentity(configPath, state.pid, state.projects, processStartIdentity);
371
+ if (sourcePath !== destinationPath)
372
+ removeRuntimeStateFile(sourcePath);
373
+ return {
374
+ path: destinationPath,
375
+ state: { ...state, process_start_identity: processStartIdentity },
376
+ };
377
+ }
378
+ function legacyRuntimeStatePath(configPath) {
379
+ return runtimeArtifactPath(`${dirname(canonicalConfigPath(configPath))}/daemon.yaml`, "state");
380
+ }
381
+ function runtimeProcessMatches(state) {
382
+ return typeof state.process_start_identity === "string"
383
+ && processIsRunning(state.pid)
384
+ && readProcessStartIdentity(state.pid) === state.process_start_identity;
385
+ }
386
+ function readProcessStartIdentity(pid) {
387
+ try {
388
+ if (process.platform === "linux") {
389
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
390
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
391
+ return fields[19] ?? null;
392
+ }
393
+ if (process.platform === "darwin" || process.platform === "freebsd") {
394
+ return execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
395
+ encoding: "utf8",
396
+ }).trim() || null;
397
+ }
398
+ if (process.platform === "win32") {
399
+ return execFileSync("powershell.exe", [
400
+ "-NoProfile",
401
+ "-NonInteractive",
402
+ "-Command",
403
+ `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().Ticks`,
404
+ ], { encoding: "utf8" }).trim() || null;
405
+ }
406
+ }
407
+ catch {
408
+ return null;
409
+ }
410
+ return null;
411
+ }
412
+ function processCommandMatchesConfig(pid, configPath) {
413
+ return inspectProcessCommandConfig(pid, configPath) === true;
414
+ }
415
+ function inspectProcessCommandConfig(pid, configPath) {
416
+ const expectedConfigPath = canonicalConfigPath(configPath);
417
+ try {
418
+ if (process.platform === "linux") {
419
+ const args = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
420
+ return commandArgsMatchConfig(args, expectedConfigPath);
421
+ }
422
+ if (process.platform === "darwin" || process.platform === "freebsd") {
423
+ const command = execFileSync("/bin/ps", ["-ww", "-o", "command=", "-p", String(pid)], {
424
+ encoding: "utf8",
425
+ }).trim();
426
+ return commandLineMatchesRuntimeConfig(command, expectedConfigPath, process.platform);
427
+ }
428
+ if (process.platform === "win32") {
429
+ const command = execFileSync("powershell.exe", [
430
+ "-NoProfile",
431
+ "-NonInteractive",
432
+ "-Command",
433
+ `(Get-CimInstance Win32_Process -Filter \"ProcessId = ${pid}\").CommandLine`,
434
+ ], { encoding: "utf8" }).trim();
435
+ return commandLineMatchesRuntimeConfig(command, expectedConfigPath, process.platform);
436
+ }
437
+ }
438
+ catch {
439
+ return null;
440
+ }
441
+ return null;
442
+ }
443
+ export function commandLineMatchesRuntimeConfig(command, configPath, platform) {
444
+ if (platform === "win32") {
445
+ return commandArgsMatchConfig(splitWindowsCommandLine(command), canonicalConfigPath(configPath));
446
+ }
447
+ return commandStringMatchesConfig(command, canonicalConfigPath(configPath));
448
+ }
449
+ function splitWindowsCommandLine(command) {
450
+ const args = [];
451
+ let index = 0;
452
+ while (index < command.length) {
453
+ while (/\s/.test(command[index] ?? ""))
454
+ index += 1;
455
+ if (index >= command.length)
456
+ break;
457
+ let argument = "";
458
+ let inQuotes = false;
459
+ while (index < command.length) {
460
+ if (!inQuotes && /\s/.test(command[index] ?? ""))
461
+ break;
462
+ let backslashes = 0;
463
+ while (command[index] === "\\") {
464
+ backslashes += 1;
465
+ index += 1;
466
+ }
467
+ if (command[index] === '"') {
468
+ argument += "\\".repeat(Math.floor(backslashes / 2));
469
+ if (backslashes % 2 === 0) {
470
+ inQuotes = !inQuotes;
471
+ }
472
+ else {
473
+ argument += '"';
474
+ }
475
+ index += 1;
476
+ continue;
477
+ }
478
+ argument += "\\".repeat(backslashes);
479
+ if (index < command.length) {
480
+ argument += command[index];
481
+ index += 1;
482
+ }
483
+ }
484
+ args.push(argument);
485
+ }
486
+ return args;
487
+ }
488
+ function commandStringMatchesConfig(command, configPath) {
489
+ const runtimeCommand = findRuntimeCommand(command);
490
+ if (!runtimeCommand)
491
+ return false;
492
+ const executablePrefix = command.slice(0, runtimeCommand.index);
493
+ if (!commandPrefixHasCloudRuntimeLauncher(executablePrefix))
494
+ return false;
495
+ if (runtimeCommand.command === "login"
496
+ && !/(?:^|\s)--foreground(?:\s|$)/.test(command.slice(runtimeCommand.index)))
497
+ return false;
498
+ const match = command.slice(runtimeCommand.index).match(/(?:^|\s)(?:--config|-c)(?:=|\s+)(.*?)(?=\s+-{1,2}[A-Za-z](?:[\w-]*)(?:=|\s|$)|$)/);
499
+ const configuredPath = match?.[1]?.trim().replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, "$1$2");
500
+ return configuredPath !== undefined && canonicalConfigPath(configuredPath) === configPath;
501
+ }
502
+ function commandArgsMatchConfig(args, configPath) {
503
+ const runtimeCommand = findRuntimeCommandInArgs(args);
504
+ if (!runtimeCommand)
505
+ return false;
506
+ if (!argsHaveCloudRuntimeLauncher(args.slice(0, runtimeCommand.index)))
507
+ return false;
508
+ const runtimeArgs = args.slice(runtimeCommand.index + 1);
509
+ if (runtimeCommand.command === "login" && !runtimeArgs.includes("--foreground"))
510
+ return false;
511
+ const configIndex = runtimeArgs.findIndex((arg) => arg === "--config" || arg === "-c");
512
+ const configuredPath = configIndex >= 0 ? runtimeArgs[configIndex + 1] : undefined;
513
+ const equalsArgument = runtimeArgs.find((arg) => arg.startsWith("--config=") || arg.startsWith("-c="))
514
+ ?.replace(/^(?:--config|-c)=/, "");
515
+ return [configuredPath, equalsArgument].some((value) => value !== undefined && canonicalConfigPath(value) === configPath);
516
+ }
517
+ function argsHaveCloudRuntimeLauncher(args) {
518
+ if (args.length === 1)
519
+ return isCloudRuntimeExecutable(args[0]);
520
+ return args.length === 2
521
+ && isNodeExecutable(args[0])
522
+ && isCloudRuntimeExecutable(args[1]);
523
+ }
524
+ function commandPrefixHasCloudRuntimeLauncher(prefix) {
525
+ const args = splitFlattenedCommandPrefix(prefix);
526
+ return args !== null && argsHaveCloudRuntimeLauncher(args);
527
+ }
528
+ function splitFlattenedCommandPrefix(prefix) {
529
+ const args = [];
530
+ const token = /\s*(?:"([^"]*)"|'([^']*)'|(\S+))/gy;
531
+ while (token.lastIndex < prefix.length) {
532
+ const match = token.exec(prefix);
533
+ if (!match)
534
+ return null;
535
+ args.push(match[1] ?? match[2] ?? match[3]);
536
+ }
537
+ return args;
538
+ }
539
+ function findRuntimeCommand(command) {
540
+ const match = /(?:^|\s)(_run|connect|login)(?=\s|$)/.exec(command);
541
+ return match ? { command: match[1], index: match.index } : null;
542
+ }
543
+ function findRuntimeCommandInArgs(args) {
544
+ const index = args.findIndex((arg) => arg === "_run" || arg === "connect" || arg === "login");
545
+ return index < 0 ? null : { command: args[index], index };
546
+ }
547
+ function isCloudRuntimeExecutable(argument) {
548
+ return /(?:^|[\\/])gsd-cloud(?:\.[cm]?js)?$/.test(argument);
549
+ }
550
+ function isNodeExecutable(argument) {
551
+ return /(?:^|[\\/])node(?:\.exe)?$/i.test(argument);
552
+ }
223
553
  function processIsRunning(pid) {
224
554
  try {
225
555
  process.kill(pid, 0);
@@ -229,36 +559,74 @@ function processIsRunning(pid) {
229
559
  return error.code === "EPERM";
230
560
  }
231
561
  }
232
- async function waitForProcessExit(pid, timeoutMs) {
562
+ async function waitForProcessExit(pid, expectedIdentity, timeoutMs) {
233
563
  const deadline = Date.now() + timeoutMs;
234
- while (processIsRunning(pid)) {
564
+ while (processMatchesIdentity(pid, expectedIdentity)) {
235
565
  if (Date.now() >= deadline)
236
566
  return false;
237
567
  await new Promise((resolve) => setTimeout(resolve, STOP_POLL_INTERVAL_MS));
238
568
  }
239
569
  return true;
240
570
  }
241
- async function terminateProcess(pid) {
242
- signalProcess(pid, "SIGTERM");
243
- if (await waitForProcessExit(pid, STOP_GRACE_PERIOD_MS))
571
+ async function terminateProcess(pid, expectedIdentity) {
572
+ if (!signalProcess(pid, expectedIdentity, "SIGTERM"))
573
+ return;
574
+ if (await waitForProcessExit(pid, expectedIdentity, STOP_GRACE_PERIOD_MS))
244
575
  return;
245
- signalProcess(pid, "SIGKILL");
246
- if (!await waitForProcessExit(pid, FORCED_STOP_TIMEOUT_MS)) {
576
+ if (!signalProcess(pid, expectedIdentity, "SIGKILL"))
577
+ return;
578
+ if (!await waitForProcessExit(pid, expectedIdentity, FORCED_STOP_TIMEOUT_MS)) {
247
579
  throw new Error(`background runtime PID ${pid} did not stop`);
248
580
  }
249
581
  }
250
- function signalProcess(pid, signal) {
582
+ async function terminateKnownChild(child) {
583
+ if (child.exitCode !== null || child.signalCode !== null)
584
+ return;
585
+ child.kill("SIGTERM");
586
+ if (await waitForChildExit(child, STOP_GRACE_PERIOD_MS))
587
+ return;
588
+ child.kill("SIGKILL");
589
+ if (!await waitForChildExit(child, FORCED_STOP_TIMEOUT_MS)) {
590
+ throw new Error(`background runtime PID ${child.pid ?? "unknown"} did not stop`);
591
+ }
592
+ }
593
+ function waitForChildExit(child, timeoutMs) {
594
+ if (child.exitCode !== null || child.signalCode !== null)
595
+ return Promise.resolve(true);
596
+ return new Promise((resolve) => {
597
+ const onExit = () => {
598
+ clearTimeout(timeout);
599
+ resolve(true);
600
+ };
601
+ const timeout = setTimeout(() => {
602
+ child.removeListener("exit", onExit);
603
+ resolve(false);
604
+ }, timeoutMs);
605
+ child.once("exit", onExit);
606
+ });
607
+ }
608
+ function signalProcess(pid, expectedIdentity, signal) {
609
+ if (!processMatchesIdentity(pid, expectedIdentity))
610
+ return false;
251
611
  try {
252
612
  process.kill(pid, signal);
613
+ return true;
253
614
  }
254
615
  catch (error) {
255
- if (error.code !== "ESRCH")
256
- throw error;
616
+ if (error.code === "ESRCH")
617
+ return false;
618
+ throw error;
257
619
  }
258
620
  }
621
+ function processMatchesIdentity(pid, expectedIdentity) {
622
+ return processIsRunning(pid) && readProcessStartIdentity(pid) === expectedIdentity;
623
+ }
259
624
  function removeRuntimeState(configPath) {
625
+ removeRuntimeStateFile(runtimeStatePath(configPath));
626
+ }
627
+ function removeRuntimeStateFile(path) {
260
628
  try {
261
- unlinkSync(runtimeStatePath(configPath));
629
+ unlinkSync(path);
262
630
  }
263
631
  catch (error) {
264
632
  if (error.code !== "ENOENT")