gdharness 0.13.35 → 0.13.36

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/build/cli.js CHANGED
@@ -15994,7 +15994,7 @@ var init_tool_definitions = __esm(() => {
15994
15994
  },
15995
15995
  {
15996
15996
  name: "editor_output",
15997
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: running is asked of the operating system, endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over.",
15997
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
15998
15998
  parameters: {
15999
15999
  severity: {
16000
16000
  type: "string",
@@ -16022,7 +16022,7 @@ var init_tool_definitions = __esm(() => {
16022
16022
  },
16023
16023
  {
16024
16024
  name: "editor_status",
16025
- description: 'Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means "not reached yet" as often as it means "no editor". True is worth waiting out; false is an editor that is not there. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade: installing moves the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with. Those games are real and running, but no tool here can reach them until the two halves match, so runtimes stays empty and runtimeConnected stays false.',
16025
+ description: 'Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means "not reached yet" as often as it means "no editor". True is worth waiting out; false is an editor that is not there. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.',
16026
16026
  parameters: {},
16027
16027
  requires: []
16028
16028
  },
@@ -36662,23 +36662,32 @@ function recordAt(path) {
36662
36662
  }
36663
36663
  function runningAs(pid) {
36664
36664
  if (process.platform === "win32") {
36665
- const line = windowsCommandLine(pid);
36666
- if (line !== null) {
36667
- return { kind: "commandLine", text: line };
36665
+ const answer = windowsCommandLine(pid);
36666
+ if (answer !== null) {
36667
+ const [, ...rest] = answer.split(`
36668
+ `);
36669
+ const line = rest.join(`
36670
+ `).trim();
36671
+ const began = startedAt(pid, answer);
36672
+ if (line !== "") {
36673
+ return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
36674
+ }
36668
36675
  }
36669
36676
  const image = windowsImage(pid);
36670
36677
  return image === null ? null : { kind: "image", text: image };
36671
36678
  }
36672
36679
  try {
36680
+ const began = startedAt(pid, null);
36681
+ const when = began === null ? {} : { startedAt: began };
36673
36682
  if (process.platform === "linux") {
36674
36683
  const raw = readFileSync14(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
36675
- return raw === "" ? null : { kind: "commandLine", text: raw };
36684
+ return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
36676
36685
  }
36677
36686
  const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
36678
36687
  encoding: "utf8",
36679
36688
  timeout: 15000
36680
36689
  }).trim();
36681
- return args === "" ? null : { kind: "commandLine", text: args };
36690
+ return args === "" ? null : { kind: "commandLine", text: args, ...when };
36682
36691
  } catch {
36683
36692
  return null;
36684
36693
  }
@@ -36717,13 +36726,30 @@ function windowsCommandLine(pid) {
36717
36726
  "-NoProfile",
36718
36727
  "-NonInteractive",
36719
36728
  "-Command",
36720
- `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`
36729
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
36721
36730
  ], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
36722
36731
  return answer === "" ? null : answer;
36723
36732
  } catch {
36724
36733
  return null;
36725
36734
  }
36726
36735
  }
36736
+ function startedAt(pid, windowsAnswer) {
36737
+ if (process.platform === "win32") {
36738
+ const when = Date.parse(windowsAnswer?.split(`
36739
+ `)[0]?.trim() ?? "");
36740
+ return Number.isNaN(when) ? null : when;
36741
+ }
36742
+ try {
36743
+ const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
36744
+ encoding: "utf8",
36745
+ timeout: 15000
36746
+ }).trim();
36747
+ const when = Date.parse(said);
36748
+ return said === "" || Number.isNaN(when) ? null : when;
36749
+ } catch {
36750
+ return null;
36751
+ }
36752
+ }
36727
36753
  function windowsImage(pid) {
36728
36754
  try {
36729
36755
  const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
@@ -36746,6 +36772,9 @@ function judgeRun(record, running, needed) {
36746
36772
  if (running === null) {
36747
36773
  return false;
36748
36774
  }
36775
+ if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
36776
+ return false;
36777
+ }
36749
36778
  const engine = record.command === undefined ? null : basename2(record.command);
36750
36779
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
36751
36780
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
@@ -36804,7 +36833,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
36804
36833
  } catch {}
36805
36834
  }
36806
36835
  }
36807
- var AN_EDITOR;
36836
+ var AN_EDITOR, SAME_RUN_WINDOW_MS = 1e4;
36808
36837
  var init_run_record = __esm(() => {
36809
36838
  init_runtime_client();
36810
36839
  AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
@@ -36840,12 +36869,18 @@ function stillRunning(run) {
36840
36869
  if (!run.throughEditor && run.process === null && run.pid !== null) {
36841
36870
  return alive(run.pid);
36842
36871
  }
36872
+ if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
36873
+ return false;
36874
+ }
36843
36875
  return true;
36844
36876
  }
36845
36877
  function runIsUp(run, editorSays) {
36846
36878
  if (run?.throughEditor !== true) {
36847
36879
  return stillRunning(run);
36848
36880
  }
36881
+ if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
36882
+ return false;
36883
+ }
36849
36884
  return editorSays ?? stillRunning(run);
36850
36885
  }
36851
36886
  function runtimeVerdict(endpoint, after) {
@@ -38599,6 +38634,10 @@ class GodotServer {
38599
38634
  budgetMs,
38600
38635
  giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
38601
38636
  });
38637
+ const going = this.currentRun();
38638
+ if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
38639
+ going.announcedPid = endpoint.pid;
38640
+ }
38602
38641
  return runtimeVerdict(endpoint, {
38603
38642
  addon: true,
38604
38643
  budgetMs,
@@ -38708,9 +38747,15 @@ class GodotServer {
38708
38747
  running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run that was recorded, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
38709
38748
  return;
38710
38749
  }
38750
+ const signalled = runningAs(running.pid);
38751
+ const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
38752
+ let landed = true;
38711
38753
  try {
38712
38754
  process.kill(running.pid);
38713
- } catch {}
38755
+ } catch {
38756
+ landed = false;
38757
+ }
38758
+ running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
38714
38759
  }
38715
38760
  spawnGame(godotPath, cmdArgs, env) {
38716
38761
  const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
@@ -39048,7 +39093,7 @@ class GodotServer {
39048
39093
  notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
39049
39094
  }
39050
39095
  if (run.transcript !== null && selected.omitted > 0) {
39051
- notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: running above is asked of the operating system, and a transcript that stops growing is a run between prints.`);
39096
+ notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
39052
39097
  }
39053
39098
  return this.jsonTextResponse({
39054
39099
  running: stillRunning(run),
@@ -40844,8 +40889,13 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
40844
40889
  with their backtraces, and a \`clean\` verdict, so a run that printed an error is one call away
40845
40890
  from being known. For a long run it also names a transcript file: read that for output and this
40846
40891
  answer for state. Never decide a run has ended by watching the file stop growing, or a process
40847
- list, or a timeout. \`running\` is asked of the operating system and \`endedBy\` says whether
40848
- gdharness ended it; a bench between prints looks exactly like a bench that died to everything else.
40892
+ list, or a timeout; a bench between prints looks exactly like a bench that died to everything else.
40893
+ \`endedBy\` says whether gdharness ended it, and \`running\` is asked of the operating system
40894
+ whenever there is a process to ask about: every run started through \`editor_run\`, and every
40895
+ editor-played run whose game announced its runtime. An editor-played run in a project with no
40896
+ runtime addon announces nothing, so there is no process id and \`running\` is the editor's answer,
40897
+ which can lag by seconds after the game has gone. \`editor_status\` names the game under
40898
+ \`runtimes\`, which does not depend on the editor answering.
40849
40899
  - One server, one editor. A second project is a second harness session with its own server, and
40850
40900
  that works: each editor is opened on its own language server and debug adapter ports, and says
40851
40901
  where it serves. Two editors on one project is the thing to refuse.
package/build/index.js CHANGED
@@ -28256,23 +28256,32 @@ function recordAt(path) {
28256
28256
  }
28257
28257
  function runningAs(pid) {
28258
28258
  if (process.platform === "win32") {
28259
- const line = windowsCommandLine(pid);
28260
- if (line !== null) {
28261
- return { kind: "commandLine", text: line };
28259
+ const answer = windowsCommandLine(pid);
28260
+ if (answer !== null) {
28261
+ const [, ...rest] = answer.split(`
28262
+ `);
28263
+ const line = rest.join(`
28264
+ `).trim();
28265
+ const began = startedAt(pid, answer);
28266
+ if (line !== "") {
28267
+ return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
28268
+ }
28262
28269
  }
28263
28270
  const image = windowsImage(pid);
28264
28271
  return image === null ? null : { kind: "image", text: image };
28265
28272
  }
28266
28273
  try {
28274
+ const began = startedAt(pid, null);
28275
+ const when = began === null ? {} : { startedAt: began };
28267
28276
  if (process.platform === "linux") {
28268
28277
  const raw = readFileSync9(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
28269
- return raw === "" ? null : { kind: "commandLine", text: raw };
28278
+ return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
28270
28279
  }
28271
28280
  const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
28272
28281
  encoding: "utf8",
28273
28282
  timeout: 15000
28274
28283
  }).trim();
28275
- return args === "" ? null : { kind: "commandLine", text: args };
28284
+ return args === "" ? null : { kind: "commandLine", text: args, ...when };
28276
28285
  } catch {
28277
28286
  return null;
28278
28287
  }
@@ -28311,13 +28320,30 @@ function windowsCommandLine(pid) {
28311
28320
  "-NoProfile",
28312
28321
  "-NonInteractive",
28313
28322
  "-Command",
28314
- `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`
28323
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
28315
28324
  ], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
28316
28325
  return answer === "" ? null : answer;
28317
28326
  } catch {
28318
28327
  return null;
28319
28328
  }
28320
28329
  }
28330
+ function startedAt(pid, windowsAnswer) {
28331
+ if (process.platform === "win32") {
28332
+ const when = Date.parse(windowsAnswer?.split(`
28333
+ `)[0]?.trim() ?? "");
28334
+ return Number.isNaN(when) ? null : when;
28335
+ }
28336
+ try {
28337
+ const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
28338
+ encoding: "utf8",
28339
+ timeout: 15000
28340
+ }).trim();
28341
+ const when = Date.parse(said);
28342
+ return said === "" || Number.isNaN(when) ? null : when;
28343
+ } catch {
28344
+ return null;
28345
+ }
28346
+ }
28321
28347
  function windowsImage(pid) {
28322
28348
  try {
28323
28349
  const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
@@ -28337,10 +28363,14 @@ function couldStillBeTheRecordedRun(record) {
28337
28363
  return judgeRun(record, runningAs(record.pid), "possible");
28338
28364
  }
28339
28365
  var AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
28366
+ var SAME_RUN_WINDOW_MS = 1e4;
28340
28367
  function judgeRun(record, running, needed) {
28341
28368
  if (running === null) {
28342
28369
  return false;
28343
28370
  }
28371
+ if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
28372
+ return false;
28373
+ }
28344
28374
  const engine = record.command === undefined ? null : basename(record.command);
28345
28375
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
28346
28376
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
@@ -29080,7 +29110,7 @@ var TOOL_SPECS = [
29080
29110
  },
29081
29111
  {
29082
29112
  name: "editor_output",
29083
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: running is asked of the operating system, endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over.",
29113
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
29084
29114
  parameters: {
29085
29115
  severity: {
29086
29116
  type: "string",
@@ -29108,7 +29138,7 @@ var TOOL_SPECS = [
29108
29138
  },
29109
29139
  {
29110
29140
  name: "editor_status",
29111
- description: 'Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means "not reached yet" as often as it means "no editor". True is worth waiting out; false is an editor that is not there. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade: installing moves the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with. Those games are real and running, but no tool here can reach them until the two halves match, so runtimes stays empty and runtimeConnected stays false.',
29141
+ description: 'Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means "not reached yet" as often as it means "no editor". True is worth waiting out; false is an editor that is not there. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.',
29112
29142
  parameters: {},
29113
29143
  requires: []
29114
29144
  },
@@ -29538,12 +29568,18 @@ function stillRunning(run) {
29538
29568
  if (!run.throughEditor && run.process === null && run.pid !== null) {
29539
29569
  return alive(run.pid);
29540
29570
  }
29571
+ if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
29572
+ return false;
29573
+ }
29541
29574
  return true;
29542
29575
  }
29543
29576
  function runIsUp(run, editorSays) {
29544
29577
  if (run?.throughEditor !== true) {
29545
29578
  return stillRunning(run);
29546
29579
  }
29580
+ if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
29581
+ return false;
29582
+ }
29547
29583
  return editorSays ?? stillRunning(run);
29548
29584
  }
29549
29585
  function runtimeVerdict(endpoint, after) {
@@ -31339,6 +31375,10 @@ class GodotServer {
31339
31375
  budgetMs,
31340
31376
  giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
31341
31377
  });
31378
+ const going = this.currentRun();
31379
+ if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
31380
+ going.announcedPid = endpoint.pid;
31381
+ }
31342
31382
  return runtimeVerdict(endpoint, {
31343
31383
  addon: true,
31344
31384
  budgetMs,
@@ -31448,9 +31488,15 @@ class GodotServer {
31448
31488
  running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run that was recorded, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
31449
31489
  return;
31450
31490
  }
31491
+ const signalled = runningAs(running.pid);
31492
+ const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
31493
+ let landed = true;
31451
31494
  try {
31452
31495
  process.kill(running.pid);
31453
- } catch {}
31496
+ } catch {
31497
+ landed = false;
31498
+ }
31499
+ running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
31454
31500
  }
31455
31501
  spawnGame(godotPath, cmdArgs, env) {
31456
31502
  const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
@@ -31788,7 +31834,7 @@ class GodotServer {
31788
31834
  notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
31789
31835
  }
31790
31836
  if (run.transcript !== null && selected.omitted > 0) {
31791
- notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: running above is asked of the operating system, and a transcript that stops growing is a run between prints.`);
31837
+ notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
31792
31838
  }
31793
31839
  return this.jsonTextResponse({
31794
31840
  running: stillRunning(run),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.35",
3
+ "version": "0.13.36",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",