gdharness 1.0.2 → 1.0.3

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/index.js CHANGED
@@ -10183,7 +10183,7 @@ import {
10183
10183
  readSync
10184
10184
  } from "node:fs";
10185
10185
  import { tmpdir as tmpdir4 } from "node:os";
10186
- import { basename as basename3, dirname as dirname6, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
10186
+ import { basename as basename3, dirname as dirname7, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
10187
10187
  import { setTimeout as delay3 } from "node:timers/promises";
10188
10188
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10189
10189
  import { promisify as promisify5 } from "node:util";
@@ -27713,20 +27713,61 @@ async function handleLSPTool(client, toolName, args) {
27713
27713
  }
27714
27714
 
27715
27715
  // src/process-children.ts
27716
- import { execFile as execFile3 } from "node:child_process";
27716
+ import { execFile as execFile3, execFileSync, spawnSync } from "node:child_process";
27717
27717
  import { basename } from "node:path";
27718
27718
  import { promisify as promisify3 } from "node:util";
27719
27719
  var run3 = promisify3(execFile3);
27720
+ function startTimesOf(pids) {
27721
+ const began = new Map;
27722
+ if (pids.length === 0) {
27723
+ return began;
27724
+ }
27725
+ try {
27726
+ if (process.platform === "win32") {
27727
+ const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
27728
+ const said = execFileSync("powershell", [
27729
+ "-NoProfile",
27730
+ "-NonInteractive",
27731
+ "-Command",
27732
+ `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
27733
+ ], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
27734
+ for (const line of said.split(`
27735
+ `)) {
27736
+ const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
27737
+ if (found?.[1] !== undefined && found[2] !== undefined) {
27738
+ began.set(Number(found[1]), Number(found[2]));
27739
+ }
27740
+ }
27741
+ return began;
27742
+ }
27743
+ const said = spawnSync("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
27744
+ encoding: "utf8",
27745
+ timeout: ASK_TIMEOUT_MS,
27746
+ env: { ...process.env, LC_ALL: "C" }
27747
+ }).stdout;
27748
+ for (const line of said.split(`
27749
+ `)) {
27750
+ const found = /^\s*(\d+)\s+(.+)$/.exec(line);
27751
+ const at = Date.parse(found?.[2]?.trim() ?? "");
27752
+ if (found?.[1] !== undefined && !Number.isNaN(at)) {
27753
+ began.set(Number(found[1]), at);
27754
+ }
27755
+ }
27756
+ } catch {}
27757
+ return began;
27758
+ }
27720
27759
  var ASK_TIMEOUT_MS = 15000;
27721
27760
  async function processTree() {
27722
27761
  try {
27723
- const printed = process.platform === "win32" ? await askWindows() : await askPosix();
27724
- return parseProcessTable(printed);
27762
+ if (process.platform === "win32") {
27763
+ return parseProcessTable(await askWindows(), true);
27764
+ }
27765
+ return parseProcessTable(await askPosix());
27725
27766
  } catch {
27726
27767
  return;
27727
27768
  }
27728
27769
  }
27729
- function parseProcessTable(printed) {
27770
+ function parseProcessTable(printed, started = false) {
27730
27771
  const tree = new Map;
27731
27772
  for (const line of printed.split(/\r?\n/)) {
27732
27773
  const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
@@ -27735,16 +27776,38 @@ function parseProcessTable(printed) {
27735
27776
  }
27736
27777
  const pid = Number(fields[1]);
27737
27778
  const parent = Number(fields[2]);
27738
- if (pid > 0) {
27739
- tree.set(pid, { parent, command: (fields[3] ?? "").trim() });
27779
+ const rest = (fields[3] ?? "").trim();
27780
+ if (pid <= 0) {
27781
+ continue;
27740
27782
  }
27783
+ if (!started) {
27784
+ tree.set(pid, { parent, command: rest });
27785
+ continue;
27786
+ }
27787
+ const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
27788
+ if (timed === null) {
27789
+ continue;
27790
+ }
27791
+ const began = Number(timed[1]);
27792
+ tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
27741
27793
  }
27742
27794
  return tree;
27743
27795
  }
27796
+ function linked(tree, child, parent) {
27797
+ const from = tree.get(child);
27798
+ const to = tree.get(parent);
27799
+ if (from === undefined || from.parent !== parent) {
27800
+ return false;
27801
+ }
27802
+ if (from.startedAt === undefined || to?.startedAt === undefined) {
27803
+ return true;
27804
+ }
27805
+ return from.startedAt >= to.startedAt;
27806
+ }
27744
27807
  function childrenIn(tree, pid) {
27745
27808
  const children = [];
27746
- for (const [child, listed] of tree) {
27747
- if (listed.parent === pid) {
27809
+ for (const child of tree.keys()) {
27810
+ if (linked(tree, child, pid)) {
27748
27811
  children.push(child);
27749
27812
  }
27750
27813
  }
@@ -27772,13 +27835,15 @@ function descendantsIn(tree, pid) {
27772
27835
  function ancestorsIn(tree, pid, root) {
27773
27836
  const chain = [];
27774
27837
  const seen = new Set([pid]);
27838
+ let child = pid;
27775
27839
  let parent = tree.get(pid)?.parent;
27776
- while (parent !== undefined && !seen.has(parent)) {
27840
+ while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
27777
27841
  chain.push(parent);
27778
27842
  if (parent === root) {
27779
27843
  break;
27780
27844
  }
27781
27845
  seen.add(parent);
27846
+ child = parent;
27782
27847
  parent = tree.get(parent)?.parent;
27783
27848
  }
27784
27849
  return chain;
@@ -27834,7 +27899,7 @@ async function askWindows() {
27834
27899
  "-NoProfile",
27835
27900
  "-NonInteractive",
27836
27901
  "-Command",
27837
- 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
27902
+ 'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
27838
27903
  ], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
27839
27904
  return stdout;
27840
27905
  }
@@ -28292,7 +28357,7 @@ function restartSettled(projectPath) {
28292
28357
  }
28293
28358
 
28294
28359
  // src/run-record.ts
28295
- import { execFileSync } from "node:child_process";
28360
+ import { execFileSync as execFileSync2 } from "node:child_process";
28296
28361
  import { mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync6 } from "node:fs";
28297
28362
  import { basename as basename2, join as join11 } from "node:path";
28298
28363
 
@@ -28300,7 +28365,7 @@ import { basename as basename2, join as join11 } from "node:path";
28300
28365
  import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
28301
28366
  import { createConnection as createConnection3 } from "node:net";
28302
28367
  import { tmpdir as tmpdir3 } from "node:os";
28303
- import { join as join10, resolve as resolve4 } from "node:path";
28368
+ import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
28304
28369
  import { setTimeout as delay2 } from "node:timers/promises";
28305
28370
 
28306
28371
  // src/tool-args.ts
@@ -28474,11 +28539,35 @@ function announcedAndGone(projectPath) {
28474
28539
  const wanted = resolve4(projectPath);
28475
28540
  return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
28476
28541
  }
28542
+ var STARTED_AFTER_ANNOUNCING_MS = 2000;
28543
+ var CONFIRMED_FOR_MS = 60000;
28544
+ var JUDGED_AFTER_MS = 60000;
28545
+ var verdicts = new Map;
28546
+ function strangersAmong(candidates, now, startTimes = startTimesOf) {
28547
+ const toJudge = candidates.filter((one) => {
28548
+ if (now - one.writtenAt < JUDGED_AFTER_MS) {
28549
+ return false;
28550
+ }
28551
+ const known = verdicts.get(one.file);
28552
+ return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
28553
+ });
28554
+ if (toJudge.length > 0) {
28555
+ const began = startTimes(toJudge.map((one) => one.pid));
28556
+ for (const one of toJudge) {
28557
+ const at = began.get(one.pid);
28558
+ const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
28559
+ verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
28560
+ }
28561
+ }
28562
+ return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
28563
+ }
28477
28564
  function announcedIn(directory) {
28478
28565
  if (!existsSync7(directory)) {
28479
28566
  return [];
28480
28567
  }
28481
28568
  const found = [];
28569
+ const candidates = [];
28570
+ const entries = [];
28482
28571
  for (const entry of readdirSync4(directory)) {
28483
28572
  const report = ERROR_REPORT_PATTERN.exec(entry);
28484
28573
  if (report) {
@@ -28492,20 +28581,60 @@ function announcedIn(directory) {
28492
28581
  const file = join10(directory, entry);
28493
28582
  const pid = Number.parseInt(match[1] ?? "", 10);
28494
28583
  const alive = processAlive(pid);
28495
- const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
28584
+ entries.push({ file, pid, alive });
28585
+ if (alive) {
28586
+ try {
28587
+ candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
28588
+ } catch {}
28589
+ }
28590
+ }
28591
+ const strangers = strangersAmong(candidates, Date.now());
28592
+ for (const { file, pid, alive } of entries) {
28593
+ const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
28496
28594
  if (announced.kind === "rubbish") {
28497
- if (!alive) {
28595
+ if (!alive || strangers.has(file)) {
28498
28596
  wentAway(file, pid);
28499
28597
  }
28500
28598
  try {
28501
28599
  unlinkSync(file);
28502
28600
  } catch {}
28601
+ verdicts.delete(file);
28503
28602
  continue;
28504
28603
  }
28505
28604
  found.push(announced);
28506
28605
  }
28606
+ const held = new Set(entries.map((one) => one.file));
28607
+ const here = join10(directory, ".");
28608
+ for (const file of verdicts.keys()) {
28609
+ if (dirname5(file) === here && !held.has(file)) {
28610
+ verdicts.delete(file);
28611
+ }
28612
+ }
28507
28613
  return found;
28508
28614
  }
28615
+ var ENDED_WITHIN_MS = 3000;
28616
+ async function announcementEnded(pid, directories = runtimeDirectories()) {
28617
+ const files = directories.map((directory) => join10(directory, `runtime-${pid}.json`)).filter((file) => existsSync7(file));
28618
+ const first = files[0];
28619
+ if (first === undefined) {
28620
+ return;
28621
+ }
28622
+ const until = Date.now() + ENDED_WITHIN_MS;
28623
+ while (processAlive(pid)) {
28624
+ const left = until - Date.now();
28625
+ if (left <= 0) {
28626
+ return;
28627
+ }
28628
+ await delay2(Math.min(LOOK_EVERY_MS, left));
28629
+ }
28630
+ wentAway(first, pid);
28631
+ for (const file of files) {
28632
+ try {
28633
+ unlinkSync(file);
28634
+ } catch {}
28635
+ verdicts.delete(file);
28636
+ }
28637
+ }
28509
28638
  function sweepErrorReport(path, pid, now = Date.now()) {
28510
28639
  if (processAlive(pid)) {
28511
28640
  return;
@@ -28814,7 +28943,7 @@ function runningAs(pid) {
28814
28943
  const raw = readFileSync11(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
28815
28944
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
28816
28945
  }
28817
- const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
28946
+ const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
28818
28947
  encoding: "utf8",
28819
28948
  timeout: 15000
28820
28949
  }).trim();
@@ -28826,20 +28955,14 @@ function runningAs(pid) {
28826
28955
  function listeningPid(port) {
28827
28956
  try {
28828
28957
  if (process.platform === "win32") {
28829
- const said = execFileSync("powershell", [
28830
- "-NoProfile",
28831
- "-NonInteractive",
28832
- "-Command",
28833
- `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
28834
- ], { encoding: "utf8", timeout: 15000 }).trim();
28835
- return /^\d+$/.test(said) ? Number(said) : null;
28958
+ return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
28836
28959
  }
28837
28960
  for (const [command, args, pattern] of [
28838
28961
  ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
28839
28962
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
28840
28963
  ]) {
28841
28964
  try {
28842
- const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
28965
+ const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
28843
28966
  const found = pattern.exec(said);
28844
28967
  if (found?.[1] !== undefined) {
28845
28968
  return Number(found[1]);
@@ -28851,9 +28974,22 @@ function listeningPid(port) {
28851
28974
  return null;
28852
28975
  }
28853
28976
  }
28977
+ function listeningPidInNetstat(printed, port) {
28978
+ for (const line of printed.split(/\r?\n/)) {
28979
+ const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
28980
+ if (fields === null) {
28981
+ continue;
28982
+ }
28983
+ const local = fields[1] ?? "";
28984
+ if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
28985
+ return Number(fields[2]);
28986
+ }
28987
+ }
28988
+ return null;
28989
+ }
28854
28990
  function windowsCommandLine(pid) {
28855
28991
  try {
28856
- const answer = execFileSync("powershell", [
28992
+ const answer = execFileSync2("powershell", [
28857
28993
  "-NoProfile",
28858
28994
  "-NonInteractive",
28859
28995
  "-Command",
@@ -28871,7 +29007,7 @@ function startedAt(pid, windowsAnswer) {
28871
29007
  return Number.isNaN(when) ? null : when;
28872
29008
  }
28873
29009
  try {
28874
- const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
29010
+ const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
28875
29011
  encoding: "utf8",
28876
29012
  timeout: 15000
28877
29013
  }).trim();
@@ -28883,7 +29019,7 @@ function startedAt(pid, windowsAnswer) {
28883
29019
  }
28884
29020
  function windowsImage(pid) {
28885
29021
  try {
28886
- const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
29022
+ const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
28887
29023
  encoding: "utf8",
28888
29024
  timeout: 15000,
28889
29025
  windowsHide: true
@@ -28969,7 +29105,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
28969
29105
 
28970
29106
  // src/setup.ts
28971
29107
  import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
28972
- import { dirname as dirname5, join as join12 } from "node:path";
29108
+ import { dirname as dirname6, join as join12 } from "node:path";
28973
29109
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
28974
29110
  var RUNTIME_AUTOLOAD = {
28975
29111
  name: "GdharnessRuntime",
@@ -29758,7 +29894,7 @@ var TOOL_SPECS = [
29758
29894
  property: {
29759
29895
  type: "string",
29760
29896
  ops: ["property", "find"],
29761
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
29897
+ description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
29762
29898
  },
29763
29899
  depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
29764
29900
  includeProperties: {
@@ -29839,7 +29975,7 @@ var TOOL_SPECS = [
29839
29975
  property: {
29840
29976
  type: "string",
29841
29977
  ops: ["set"],
29842
- description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
29978
+ description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
29843
29979
  },
29844
29980
  value: {
29845
29981
  ops: ["set"],
@@ -29849,7 +29985,7 @@ var TOOL_SPECS = [
29849
29985
  method: {
29850
29986
  type: "string",
29851
29987
  ops: ["call"],
29852
- description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, and a list or a map is stepped into by index or key, "_game:run:roster:0:retire".'
29988
+ description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
29853
29989
  },
29854
29990
  args: {
29855
29991
  type: "array",
@@ -29909,7 +30045,7 @@ var TOOL_SPECS = [
29909
30045
  pressed: {
29910
30046
  type: "boolean",
29911
30047
  ops: ["action", "key", "mouse_click"],
29912
- description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
30048
+ description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
29913
30049
  },
29914
30050
  strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
29915
30051
  keycode: {
@@ -29987,7 +30123,7 @@ var TOOL_SPECS = [
29987
30123
  requires: ["text"]
29988
30124
  },
29989
30125
  mouse_click: {
29990
- summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
30126
+ summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
29991
30127
  requires: ["x", "y"]
29992
30128
  },
29993
30129
  mouse_motion: {
@@ -30009,7 +30145,11 @@ var TOOL_SPECS = [
30009
30145
  },
30010
30146
  nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
30011
30147
  signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
30012
- property: { type: "string", ops: ["until"], description: "until: the property name." },
30148
+ property: {
30149
+ type: "string",
30150
+ ops: ["until"],
30151
+ description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
30152
+ },
30013
30153
  value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
30014
30154
  says: {
30015
30155
  type: "string",
@@ -30134,7 +30274,7 @@ function buildToolDefinitions() {
30134
30274
  var UPDATE_NOTICE_EVERY = 500;
30135
30275
  var FEEDBACK_NOTICE_EVERY = 250;
30136
30276
  var run5 = promisify5(execFile5);
30137
- var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
30277
+ var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
30138
30278
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
30139
30279
  var HOLD_PING_MS = 2000;
30140
30280
  var SLOWEST_FRAME_RATE = 20;
@@ -31396,11 +31536,11 @@ class GodotServer {
31396
31536
  if (!engine.ok) {
31397
31537
  return engine.response;
31398
31538
  }
31399
- const folder = dirname6(output.absolutePath);
31539
+ const folder = dirname7(output.absolutePath);
31400
31540
  try {
31401
31541
  mkdirSync7(folder, { recursive: true });
31402
31542
  } catch (error) {
31403
- return this.createErrorResponse(`Cannot create ${dirname6(output.relativePath)} to export into: ${errorMessage(error)}`);
31543
+ return this.createErrorResponse(`Cannot create ${dirname7(output.relativePath)} to export into: ${errorMessage(error)}`);
31404
31544
  }
31405
31545
  const debug = readBoolean(args, "debug") ?? false;
31406
31546
  const exportLogs = mkdtempSync2(join13(tmpdir4(), "gdharness-export-"));
@@ -31854,11 +31994,14 @@ class GodotServer {
31854
31994
  return null;
31855
31995
  }
31856
31996
  try {
31857
- const answer = await Promise.race([
31858
- this.godotBridge.invokeTool("playing_status", {}),
31859
- delay3(PLAYING_STATUS_MS).then(() => null)
31860
- ]);
31997
+ const askedAt = Date.now();
31998
+ const asked = this.godotBridge.invokeTool("playing_status", {});
31999
+ const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
31861
32000
  if (answer === null) {
32001
+ this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
32002
+ asked.then(() => {
32003
+ this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
32004
+ }).catch(() => {});
31862
32005
  return null;
31863
32006
  }
31864
32007
  return {
@@ -32178,6 +32321,9 @@ class GodotServer {
32178
32321
  let asking = null;
32179
32322
  let editorAskedAt = 0;
32180
32323
  let editorSaysGoing = true;
32324
+ let editorAsks = 0;
32325
+ const waitBegan = Date.now();
32326
+ const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
32181
32327
  const endpoint = await announcedSince(projectPath, before, {
32182
32328
  budgetMs,
32183
32329
  accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
@@ -32191,6 +32337,7 @@ class GodotServer {
32191
32337
  }
32192
32338
  if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
32193
32339
  editorAskedAt = Date.now();
32340
+ editorAsks += 1;
32194
32341
  asking = this.runStillGoing(run).then((going) => {
32195
32342
  editorSaysGoing = going;
32196
32343
  }).finally(() => {
@@ -32200,6 +32347,7 @@ class GodotServer {
32200
32347
  return !editorSaysGoing;
32201
32348
  }
32202
32349
  });
32350
+ this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
32203
32351
  const going = this.currentRun();
32204
32352
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
32205
32353
  going.announcedPid = endpoint.pid;
@@ -32876,11 +33024,19 @@ class GodotServer {
32876
33024
  this.drainEditorOutput(stopped);
32877
33025
  this.drainTranscript(stopped);
32878
33026
  const wasRunning = await this.runStillGoing(stopped);
32879
- const endedPid = stopped.pid ?? this.announcedPidOf(stopped) ?? null;
33027
+ const announcedPid = this.announcedPidOf(stopped) ?? null;
33028
+ const endedPid = stopped.pid ?? announcedPid;
32880
33029
  const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
32881
33030
  this.logDebug("Stopping the running game");
32882
33031
  await this.endActiveGame("editor_run stop");
32883
33032
  const ended = children === null ? null : endChildrenAmong(children);
33033
+ if (wasRunning) {
33034
+ for (const pid of new Set([endedPid, announcedPid])) {
33035
+ if (pid !== null) {
33036
+ await announcementEnded(pid);
33037
+ }
33038
+ }
33039
+ }
32884
33040
  return this.jsonTextResponse({
32885
33041
  stopped: true,
32886
33042
  through: stopped.throughEditor ? "editor" : "gdharness",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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",