summrise-agent 1.2.492 → 1.2.494

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/bin/summrise.js CHANGED
@@ -39,6 +39,7 @@ exports.psArgv = psArgv;
39
39
  exports.lnkIdentity = lnkIdentity;
40
40
  exports.deskShortcutRepairPs = deskShortcutRepairPs;
41
41
  exports.startDesktopPs = startDesktopPs;
42
+ exports.ensureDesktopPs = ensureDesktopPs;
42
43
  exports.desktopTaskPs = desktopTaskPs;
43
44
  exports.desktopStartPs = desktopStartPs;
44
45
  exports.parseAgentPort = parseAgentPort;
@@ -534,16 +535,182 @@ function deskShortcutRepairPs(scriptsQ, deskDirQ, sink) {
534
535
  `foreach ($dRx in @('summrise-desktop.exe','summrise-tray.exe')) { $dRp = '${deskDirQ}\\' + $dRx; if (Test-Path $dRp) { try { Remove-Item -Force -ErrorAction Stop $dRp; ('desk: removed retired ' + $dRx) | ${sink} } catch { ('desk: retired ' + $dRx + ' locked, kept') | ${sink} } } }`,
535
536
  ];
536
537
  }
537
- // exported: the start-desktop.ps1 launcher (the SummriseDesktop onlogon task +
538
- // desktop Summrise.lnk both call it). Launches the Electron shell from
539
- // components\summrise-desktop-electron\ with the working directory set there
540
- // (electron . resolves src/main.js via package.json main). The script is
541
- // intentionally tiny + ASCII-only (system-locale PS). unit-tested.
542
- function startDesktopPs(deskDirQ) {
538
+ // ── THE DESKTOP SHELL'S RECORD: WHAT THE LAUNCHERS CAN SEE, AND WHAT THEY CANNOT ──
539
+ //
540
+ // MEASURED on desktop-14rjcr8 (2026-09-28). The desktop shell was KILLED every ten minutes
541
+ // by `<ExecutionTimeLimit>PT10M</ExecutionTimeLimit>` on the SummriseDesktop task: the
542
+ // launcher ends on `& electron.exe .` — the PowerShell CALL OPERATOR, which WAITS — so the
543
+ // shell lives inside the task's process tree and the scheduler killed the whole tree when
544
+ // the limit expired. The guarded 5-minute pulse then revived it, correctly, and ten minutes
545
+ // later it died again. `logs\startup.log` held 4,015 lines and answered ZERO for `desktop`,
546
+ // ZERO for `electron` and ZERO for `ExecutionTimeLimit`. Nothing in the product recorded
547
+ // it; the operator said "还一直在重启呢" four times, and the answer had to be recovered from
548
+ // the Task Scheduler's own operational log, where Summrise is one line among a stream of
549
+ // Edge update tasks.
550
+ //
551
+ // THE SHAPE ALREADY EXISTED ONE PROCESS OVER. The agent has exactly the right sentence
552
+ // about itself in its own run journal — `runstate.rs`: "previous run DID NOT EXIT CLEANLY —
553
+ // REPLACED by a restart" — and the shell, which is the process that IS killed from outside,
554
+ // had no counterpart. These are that counterpart.
555
+ //
556
+ // WHERE THE LINES GO, AND WHY THERE. `logs\startup.log`, under the SAME registry-first DataDir
557
+ // the agent's own `paths.rs` resolves (`DATA_DIR` above — never a hardcoded ProgramData, and
558
+ // the value is baked in at generation time by the process that DID the lookup). `startup.log`
559
+ // is the file the operator actually grepped. A record in a second file nobody knows to open
560
+ // would be tonight's failure one level down.
561
+ //
562
+ // EVERY LINE CONTAINS THE WHOLE WORD `desktop` — the search that failed was
563
+ // `findstr /i desktop ...\logs\startup.log` — and names `electron` wherever it is the subject.
564
+ //
565
+ // AND THE ONE CASE THAT CANNOT BE RECORDED IS WRITTEN DOWN WHERE IT HAPPENS, in
566
+ // `startDesktopPs` below: a Task Scheduler kill takes the logging process with it, so it
567
+ // leaves NO exit code, ever. The next pulse reports that ABSENCE out loud.
568
+ /** The desktop shell's launcher — and the only place in the product that can see it EXIT.
569
+ *
570
+ * `exit code N` is what tells a crash from a kill, and GETTING IT COST A MEASUREMENT. The
571
+ * obvious form was `& "$dir\node_modules\electron\dist\electron.exe" .` then `$LASTEXITCODE`,
572
+ * and it does not work: **electron.exe is a GUI-subsystem binary and Windows PowerShell 5.1
573
+ * does not set `$LASTEXITCODE` for one.** Measured on desktop-14rjcr8, 2026-09-28:
574
+ *
575
+ * & cmd /c 'exit 7' -> $LASTEXITCODE = 7
576
+ * & electron.exe --version -> returned in 0.05s, $LASTEXITCODE STILL 7 (unchanged)
577
+ * [Diagnostics.Process] variant -> waited, .ExitCode = 0
578
+ *
579
+ * The first version of this file used `&`, and the line it wrote on a real restart read
580
+ * `desktop: electron.exe exited (start=1790589628 exit code lived=1s)` — an empty field
581
+ * where a number belongs. That is the same defect this whole record exists to remove (a
582
+ * surface reporting a step in the grammar of an outcome), so the launch goes through
583
+ * `System.Diagnostics.Process` with `UseShellExecute = $false`: the SAME CreateProcess the
584
+ * call operator uses, same inherited handles, same working directory — plus `WaitForExit()`
585
+ * and a real `.ExitCode`.
586
+ *
587
+ * `WaitForExit()` IS LOAD-BEARING BEYOND THE EXIT CODE: it is why this PowerShell stays
588
+ * alive as the shell's parent, which is why the shell lives inside the task's process tree.
589
+ *
590
+ * WHAT IT CANNOT SEE, AND IT IS THE CASE THAT HAPPENED. The task's action is
591
+ * desktop-pulse.vbs -> ensure-desktop.ps1 -> THIS FILE, and Task Scheduler's
592
+ * `<ExecutionTimeLimit>` terminates that whole tree — electron, this PowerShell and the
593
+ * wscript above it. So a scheduler kill records NOTHING here, and neither do `taskkill /T`
594
+ * and a machine going down: the process that would write the line dies with the shell. On
595
+ * 2026-09-28 PT10M killed the shell every ten minutes and this file, as it stood, said
596
+ * nothing at all. The signature is therefore the ABSENCE of an `exited (start=N …)` line for
597
+ * a start the watchdog watched disappear, and `ensureDesktopPs` says those words on the next
598
+ * pulse rather than leaving the gap to be interpreted.
599
+ *
600
+ * Launches the Electron shell from components\summrise-desktop-electron\ with the working
601
+ * directory set there (electron . resolves src/main.js via package.json main). Intentionally
602
+ * small + ASCII-only (system-locale PS). unit-tested. */
603
+ function startDesktopPs(deskDirQ, logsQ) {
543
604
  return [
605
+ "# written by `summrise setup` / `summrise desktop` / `summrise update` -- start the",
606
+ "# Electron shell, and leave a record of the launch and of the exit. The comments stay",
607
+ "# IN the file on purpose: this is what an operator reads when the window is gone.",
608
+ "#",
609
+ "# WHAT THIS CANNOT SEE. The launch below WAITS, so it returns with the shell's exit code",
610
+ "# when the shell exits OR crashes. It does NOT return when the whole process tree is",
611
+ "# killed from outside -- a Task Scheduler execution limit, a `taskkill /T`, a machine",
612
+ "# going down -- because this PowerShell dies first, and then NO exit code is written at",
613
+ "# all. ensure-desktop.ps1 reports that absence on the next pulse; a log that recorded",
614
+ "# only exits would silently omit the one death that actually happens here.",
615
+ "#",
616
+ "# WHY NOT `& electron.exe .` AND $LASTEXITCODE (measured on desktop-14rjcr8, 2026-09-28):",
617
+ "# electron.exe is a GUI-subsystem binary and Windows PowerShell 5.1 does not set",
618
+ "# $LASTEXITCODE for one. `& cmd /c 'exit 7'` set it to 7; the next `& electron.exe",
619
+ "# --version` left it at 7, unchanged. The first version of this file used `&` and wrote",
620
+ "# `exit code ` with the number missing -- a field that looks like a value and is not one.",
621
+ "# System.Diagnostics.Process is the same CreateProcess, and its ExitCode is real.",
544
622
  `$dir = '${deskDirQ}'`,
545
- `Set-Location $dir`,
546
- `& "$dir\\node_modules\\electron\\dist\\electron.exe" .`,
623
+ `$log = '${logsQ}\\startup.log'`,
624
+ "New-Item -ItemType Directory -Force -Path (Split-Path $log) | Out-Null",
625
+ "$E = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()",
626
+ "Add-Content -Path $log -Value ('[' + (Get-Date -Format o) + '] desktop: electron.exe launching from ' + $dir + ' (start=' + $E + ')') -ErrorAction SilentlyContinue",
627
+ "Set-Location $dir",
628
+ "$code = $null",
629
+ "try {",
630
+ " $si = New-Object System.Diagnostics.ProcessStartInfo",
631
+ " $si.FileName = \"$dir\\node_modules\\electron\\dist\\electron.exe\"",
632
+ " $si.Arguments = '.'",
633
+ " $si.WorkingDirectory = $dir",
634
+ " $si.UseShellExecute = $false",
635
+ " $p = [System.Diagnostics.Process]::Start($si)",
636
+ " $p.WaitForExit()",
637
+ " $code = $p.ExitCode",
638
+ "} catch { $code = $null }",
639
+ "$L = [int]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $E)",
640
+ "if ($null -ne $code) {",
641
+ " Add-Content -Path $log -Value ('[' + (Get-Date -Format o) + '] desktop: electron.exe exited (start=' + $E + ' exit code ' + $code + ' lived=' + $L + 's)') -ErrorAction SilentlyContinue",
642
+ "} else {",
643
+ " Add-Content -Path $log -Value ('[' + (Get-Date -Format o) + '] desktop: electron.exe DID NOT RUN (start=' + $E + ') -- the launcher could not start it, so there is no exit code') -ErrorAction SilentlyContinue",
644
+ "}",
645
+ ];
646
+ }
647
+ /** The desktop shell's WATCHDOG: start the shell when none is running, and RECORD WHICH OF
648
+ * THE TWO THINGS HAPPENED — in the words a person would search for.
649
+ *
650
+ * Run every 5 minutes by `desktop-pulse.vbs` under the SummriseDesktop task, and once at
651
+ * logon. `desktop-pulse.vbs` itself takes no decision and carries no record.
652
+ *
653
+ * WRITTEN FROM ONE BUILDER, BY ALL THREE PATHS THAT WRITE IT — `summrise setup`,
654
+ * `summrise desktop` and the update swap — because a second copy of this text is a copy
655
+ * that drifts, and the drift would silently revert the record on whichever path was not
656
+ * edited (the shape `installer_integrity.rs` already holds the task definition against).
657
+ *
658
+ * THE HEALTHY CASE IS LOGGED, ONCE AN HOUR, AND BOTH HALVES OF THAT ARE DELIBERATE.
659
+ *
660
+ * A line per 5-minute pulse is 288 lines a day of "still fine" in `startup.log`, which is
661
+ * the AGENT's boot record and rotates at 1 MB (`main.rs::log_line`). That heartbeat would
662
+ * evict the history of a real incident — the opposite of the point — and an operator
663
+ * grepping `desktop` after a restart would have to wade through it to find the restart.
664
+ *
665
+ * Writing NOTHING in the healthy case is worse in the other direction: a watchdog that has
666
+ * stopped and a watchdog that is fine would look identical, because both are SILENCE — this
667
+ * repository's own recorded defect class, where "if nothing was checked, say that" is the
668
+ * honest form. So: ONE line an hour (~2 KB a day), deduplicated against the LOG ITSELF
669
+ * rather than a state file (the record is the state, so there is nothing to get out of
670
+ * sync), and a gap longer than about two hours is the signal that the pulse itself stopped.
671
+ *
672
+ * THE DECISION IS STATED AS THE OBSERVATION, NOT AS THE STEP: "no electron shell is
673
+ * running" is what the pulse SAW; the launch it then performs is the consequence. When the
674
+ * shell that disappeared left no `exited (start=N …)` line, this says so in words — that is
675
+ * the scheduler-kill signature, and it is exactly the case `startDesktopPs` cannot record.
676
+ *
677
+ * ASCII-only (system-locale PS). unit-tested. */
678
+ function ensureDesktopPs(scriptsQ, logsQ) {
679
+ return [
680
+ "# written by `summrise setup` / `summrise desktop` / `summrise update` -- the desktop",
681
+ "# shell's watchdog, run every 5 minutes by desktop-pulse.vbs under SummriseDesktop.",
682
+ "# Start the shell if none is running, and RECORD WHICH OF THE TWO THINGS HAPPENED:",
683
+ "# silence is not a status. The builder in src/summrise.ts carries the full reasoning,",
684
+ "# including why the healthy case is one line an hour and not one per pulse.",
685
+ `$log = '${logsQ}\\startup.log'`,
686
+ "New-Item -ItemType Directory -Force -Path (Split-Path $log) | Out-Null",
687
+ "$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()",
688
+ "$tail = @(Get-Content -Path $log -Tail 400 -ErrorAction SilentlyContinue)",
689
+ "$p = @(Get-Process electron -ErrorAction SilentlyContinue)",
690
+ "if ($p.Count -gt 0) {",
691
+ " $h = 'hour=' + (Get-Date -Format 'yyyy-MM-ddTHH')",
692
+ " if (-not ($tail -match ('desktop: electron shell alive .*' + $h))) {",
693
+ " $up = ($p | Sort-Object StartTime | Select-Object -First 1).StartTime",
694
+ " Add-Content -Path $log -Value ('[' + (Get-Date -Format o) + '] desktop: electron shell alive -- ' + $p.Count + ' process(es), oldest since ' + $up + ' ' + $h) -ErrorAction SilentlyContinue",
695
+ " }",
696
+ " exit",
697
+ "}",
698
+ "$was = 'no earlier launch is recorded in the log tail'",
699
+ // THE PATTERN MUST MATCH THE LINE `startDesktopPs` ACTUALLY WRITES, and the first version of
700
+ // it did NOT: it read `launching \(start=` while the launcher writes `launching from <dir>
701
+ // (start=`. Every pulse therefore fell through to "no earlier launch is recorded in the log
702
+ // tail" and the previous shell's lifetime was never reported. MEASURED on desktop-14rjcr8 —
703
+ // the pulse said exactly that at 18:06:53 while the launching line it was looking for sat
704
+ // thirty lines above it in the same file. The ` .*` is the fix, and test/cli.test.mjs now
705
+ // runs THIS pattern against a REAL captured line, which is the check that would have caught it.
706
+ "$prev = $tail | Select-String -Pattern 'desktop: electron.exe launching .*\\(start=(\\d+)\\)' | Select-Object -Last 1",
707
+ "if ($prev -and ($prev.Line -match '\\(start=(\\d+)\\)')) {",
708
+ " $E = [int]$Matches[1]",
709
+ " if ($tail -match ('desktop: electron\\.exe exited \\(start=' + $E + ' ')) { $was = 'the previous shell exited and its code is in the exited (start=' + $E + ') line' }",
710
+ " else { $was = 'the previous shell lived ' + ([int]$now - $E) + 's and left NO exit line -- it was KILLED, not exited: a Task Scheduler stop, a taskkill /T or a reboot leaves no exit code (start=' + $E + ')' }",
711
+ "}",
712
+ "Add-Content -Path $log -Value ('[' + (Get-Date -Format o) + '] desktop: no electron shell is running -- the watchdog is starting it; previous: ' + $was) -ErrorAction SilentlyContinue",
713
+ `& powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptsQ}\\start-desktop.ps1"`,
547
714
  ];
548
715
  }
549
716
  /** The desktop shell's watchdog pair + its task, as ONE PowerShell script.
@@ -555,14 +722,23 @@ function startDesktopPs(deskDirQ) {
555
722
  * never ran setup can still get a working task by asking for the window. Idempotent:
556
723
  * ensure-desktop.ps1 exits when electron is already alive, so the 5-minute trigger never
557
724
  * steals focus. unit-tested. */
558
- function desktopTaskPs(installQ) {
725
+ function desktopTaskPs(installQ, logsQ) {
559
726
  return [
560
727
  "# written by `summrise setup` / `summrise desktop` -- the desktop shell's task + watchdog.",
561
728
  "$ErrorActionPreference = 'Stop'",
562
729
  `$q = '${installQ}'`,
563
730
  `$en = Join-Path $q 'scripts\\ensure-desktop.ps1'`,
564
731
  `$vb = Join-Path $q 'scripts\\desktop-pulse.vbs'`,
565
- `Set-Content -Path $en -Value ('if (Get-Process electron -ErrorAction SilentlyContinue) { exit }; & powershell -NoProfile -ExecutionPolicy Bypass -File "' + $q + '\\scripts\\start-desktop.ps1"') -Force`,
732
+ // THE WATCHDOG'S BODY COMES FROM ITS OWN BUILDER, not from a second hand-typed copy of
733
+ // the same one-liner. Three paths write this file — `summrise setup`, `summrise desktop`
734
+ // and the update swap — and a copy that drifts is a copy that silently reverts the
735
+ // shell's record on whichever path was not edited. A single-quoted here-string
736
+ // (`@'` … `'@`, each alone on its line) carries it byte for byte: nothing in the body is
737
+ // expanded, and nothing in the body can end it early.
738
+ `$enBody = @'`,
739
+ ...ensureDesktopPs(`${installQ}\\scripts`, logsQ),
740
+ `'@`,
741
+ `Set-Content -Path $en -Value $enBody -Force`,
566
742
  `Set-Content -Path $vb -Value ('CreateObject("WScript.Shell").Run "powershell -NoProfile -ExecutionPolicy Bypass -File " & Chr(34) & "' + $en + '" & Chr(34), 0, False') -Force`,
567
743
  `$da = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument ('"' + $vb + '"') -WorkingDirectory $q`,
568
744
  `$dt1 = New-ScheduledTaskTrigger -AtLogOn`,
@@ -2292,7 +2468,7 @@ const commands = {
2292
2468
  // repetition is what reborns a dead shell.
2293
2469
  try {
2294
2470
  const reg = path.join(SCRIPTS_DIR, "register-desktop-task.ps1");
2295
- fs.writeFileSync(reg, desktopTaskPs(DIR).join("\r\n") + "\r\n");
2471
+ fs.writeFileSync(reg, desktopTaskPs((0, exports.psq)(DIR), (0, exports.psq)(LOGS_DIR)).join("\r\n") + "\r\n");
2296
2472
  // THE STATUS WAS DISCARDED AND THE SUCCESS LINE WAS UNCONDITIONAL, which is the defect the AGENT
2297
2473
  // task's check ten lines below names in its own comment: "audit #7: used to claim success
2298
2474
  // regardless". Worse, the `catch` below could not fire for this — `sh()` RETURNS, it does not
@@ -2323,7 +2499,7 @@ const commands = {
2323
2499
  // SummriseDesktop onlogon task + desktop Summrise.lnk both call it). Was never
2324
2500
  // written before — a real gap that left the shell unlaunchable.
2325
2501
  fs.mkdirSync(SCRIPTS_DIR, { recursive: true });
2326
- fs.writeFileSync(path.join(SCRIPTS_DIR, "start-desktop.ps1"), startDesktopPs((0, exports.psq)(DESK_DIR)).join("\r\n"), "utf8");
2502
+ fs.writeFileSync(path.join(SCRIPTS_DIR, "start-desktop.ps1"), startDesktopPs((0, exports.psq)(DESK_DIR), (0, exports.psq)(LOGS_DIR)).join("\r\n"), "utf8");
2327
2503
  console.log("setup: scripts\\start-desktop.ps1 written");
2328
2504
  // Register boot-start task (SYSTEM) and kick it once; the agent's own
2329
2505
  // first-run flow registers the device with the console using the key.
@@ -2560,7 +2736,7 @@ const commands = {
2560
2736
  if (!fs.existsSync(reg)) {
2561
2737
  // A machine that never ran setup still gets a working task: the same script setup
2562
2738
  // writes, from the same builder, so the two cannot drift.
2563
- fs.writeFileSync(reg, desktopTaskPs(DIR).join("\r\n") + "\r\n");
2739
+ fs.writeFileSync(reg, desktopTaskPs((0, exports.psq)(DIR), (0, exports.psq)(LOGS_DIR)).join("\r\n") + "\r\n");
2564
2740
  }
2565
2741
  // THE START SCRIPT GOES IN A FILE, AND ONLY IT DECIDES WHETHER TO REGISTER. Spawning with
2566
2742
  // `shell: true` means cmd.exe, and **cmd splits a command on `&`** — the PowerShell call
@@ -3010,7 +3186,7 @@ const commands = {
3010
3186
  // the swap script's migration, and a headless install simply never
3011
3187
  // calls the launcher (shortcut repair is Test-Path guarded anyway).
3012
3188
  fs.mkdirSync(SCRIPTS_DIR, { recursive: true });
3013
- fs.writeFileSync(path.join(SCRIPTS_DIR, "start-desktop.ps1"), startDesktopPs((0, exports.psq)(DESK_DIR)).join("\r\n"), "utf8");
3189
+ fs.writeFileSync(path.join(SCRIPTS_DIR, "start-desktop.ps1"), startDesktopPs((0, exports.psq)(DESK_DIR), (0, exports.psq)(LOGS_DIR)).join("\r\n"), "utf8");
3014
3190
  // round-298: record the release version on a PROVABLY successful swap
3015
3191
  // so agent_update (which reads <install>/.summrise-release as its local
3016
3192
  // version) reports up_to_date instead of re-swapping every call.
@@ -3101,7 +3277,15 @@ const commands = {
3101
3277
  // wscript wrapper runs it with no console flash.
3102
3278
  `$en1 = '${q}\\scripts\\ensure-desktop.ps1'`,
3103
3279
  `$vb1 = '${q}\\scripts\\desktop-pulse.vbs'`,
3104
- `Set-Content -Path $en1 -Value 'if (Get-Process electron -ErrorAction SilentlyContinue) { exit }; & powershell -NoProfile -ExecutionPolicy Bypass -File "${q}\\scripts\\start-desktop.ps1"' -Force`,
3280
+ // THE SAME BUILDER `setup` USES, and this is the path that matters most: an update is
3281
+ // the ONLY mechanism that reaches a machine already installed, so a watchdog written
3282
+ // here from a second, hand-typed copy would silently overwrite the instrumented one on
3283
+ // every fleet device while `setup` looked correct. `$en1` is assigned above and the
3284
+ // here-string needs no interpolation, so the body travels verbatim.
3285
+ `$enBody = @'`,
3286
+ ...ensureDesktopPs(`${q}\\scripts`, qd),
3287
+ `'@`,
3288
+ `Set-Content -Path $en1 -Value $enBody -Force`,
3105
3289
  `Set-Content -Path $vb1 -Value 'CreateObject("WScript.Shell").Run "powershell -NoProfile -ExecutionPolicy Bypass -File " & Chr(34) & "${q}\\scripts\\ensure-desktop.ps1" & Chr(34), 0, False' -Force`,
3106
3290
  `if ($null -ne (Get-ScheduledTask -TaskName 'SummriseDesktop' -ErrorAction SilentlyContinue)) {`,
3107
3291
  ` $da = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument ('"' + $vb1 + '"') -WorkingDirectory '${q}'`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summrise-agent",
3
- "version": "1.2.492",
3
+ "version": "1.2.494",
4
4
  "description": "Summrise Agent — Windows device MCP server (terminal/SSH/serial) + panel. Self-hosted npm distribution.",
5
5
  "//build": "唯一构建路径:在本目录执行 npm run build。它是自包含的:经 npm exec 按需拉取 typescript@5,无需预装任何 node_modules(干净检出可跑;旧脚本引用 ../summrise-desktop-electron/node_modules/.bin/tsc,干净检出必 ENOENT)。必须用 npm exec --package … -- tsc 的形式:npx 会吃掉传给 tsc 的 -p/--outDir 等参数(round-435:npx 写法从未真正跑通过)。先编 electron TS(--noCheck 纯 emit,与 release.yml 一致)同步两处 src/*.js,再编 src/summrise.ts 到 bin/summrise.js 并校验 round-298 标记(弱 oracle,只证某次构建发生,新鲜性由 publish-release.sh 的 tsc 重编 + cmp 门保证)。",
6
6
  "bin": {
Binary file
@@ -952,8 +952,8 @@ if (gotTheLock) {
952
952
  // whatever the agent turned out to be. show:false keeps it off screen until the
953
953
  // first document has painted (ready-to-show, below); backgroundColor paints that
954
954
  // first frame in the shell's own ink instead of Chromium's white, which is the one
955
- // of the two documents that had NOT already fixed this (WAIT_HTML hardcodes
956
- // background:#111, and the SPA's chrome is dark).
955
+ // of the two documents that had NOT already fixed this (the wait page
956
+ // hardcoded background:#111, and the SPA's chrome is dark).
957
957
  show: false,
958
958
  backgroundColor: "#111",
959
959
  // Taskbar + title-bar icon: without this Windows shows the stock
@@ -1039,38 +1039,130 @@ if (gotTheLock) {
1039
1039
  // and the watchdog could not fire (the device stayed stuck in silence,
1040
1040
  // the exact failure the watchdog exists to prevent).
1041
1041
  const agentReady = async () => agentResponds(800);
1042
- // stage-n: the wait page now carries a "Start Agent" action — the
1043
- // header comment promised it but it never existed. The button calls the
1044
- // shell's own /api/shell/start-agent (schtasks /run SummriseAgent — the
1045
- // only sanctioned spawn path), then keeps polling until the agent is
1046
- // up. This turns a dead-end white screen into a recoverable state when
1047
- // the SummriseAgent task is down (crash, stopped task, update mid-swap).
1048
- const WAIT_HTML = `<!doctype html><meta charset="utf-8"><title>Summrise</title>
1049
- <style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}div{text-align:center}p{color:#999}button{margin-top:18px;padding:10px 22px;font-size:15px;border-radius:8px;border:1px solid #444;background:#222;color:#eee;cursor:pointer}button:hover{background:#333}button:disabled{opacity:.5;cursor:default}</style>
1042
+ // stage-n: the wait page carries a "Start Agent" action — the header
1043
+ // comment promised it but it never existed. The button calls the shell's
1044
+ // own /api/shell/start-agent (schtasks /run SummriseAgent — the only
1045
+ // sanctioned spawn path). This turns a dead-end white screen into a
1046
+ // recoverable state when the SummriseAgent task is down (crash, stopped
1047
+ // task, update mid-swap).
1048
+ //
1049
+ // REWRITTEN (round 276). Three defects, all of them measured on the
1050
+ // device's own window at 1200x761, and all three are the same defect
1051
+ // wearing different clothes: THE PAGE ASSERTED THINGS IT HAD NOT CHECKED.
1052
+ //
1053
+ // 1. The heading said "Summrise Agent is not running". Nothing on this
1054
+ // path checked that. `loadDesktop` reaches this page when a request
1055
+ // to one address does not ANSWER — the agent may be running and
1056
+ // wedged, on another port, mid-restart, or still booting. The file's
1057
+ // own comment above `agentReady` says a wedged-but-listening agent
1058
+ // "holds the port yet never answers", which is the case the heading
1059
+ // got wrong. It now names the observation (a request went out and
1060
+ // nothing came back) and the address it went to.
1061
+ // 2. The subtext said "waiting for …" and then never showed the wait.
1062
+ // No attempt count, no clock — so a four-second boot and a six-hour
1063
+ // corpse rendered identically. It now carries both, and both TICK
1064
+ // (below), so the operator can tell patience from action.
1065
+ // 3. The button threw its answer away. `/api/shell/start-agent` returns
1066
+ // `{ok, error}` — `startAgentTask()` is `runSchtasks([...])` — and
1067
+ // the page did `await fetch(...)` and printed "started" regardless,
1068
+ // so a REFUSED start read exactly like an accepted one. It reads the
1069
+ // body now and says which happened.
1070
+ //
1071
+ // AND THE PAGE OWNS ITS OWN CLOCK, which is why `loadWaitPage` below will
1072
+ // not re-load a page that is already showing: a reload every 2-30 s wiped
1073
+ // the button's answer before it could be read, and re-rendered the
1074
+ // evidence from zero. The page polls the shell's own /api/shell/agent-status
1075
+ // (the shell is alive by construction — it is what draws this page) and
1076
+ // navigates itself to the SPA the moment the agent answers.
1077
+ //
1078
+ // THE COLOURS ARE THE DARK THEME'S TOKEN VALUES, LITERALLY, AND THAT IS
1079
+ // DELIBERATE: this page is drawn while the agent is mute, so it cannot
1080
+ // load the SPA's token sheet — but a fallback using its own greys is a
1081
+ // different product's screen. Values copied from
1082
+ // panel-react/src/styles/tokens.css `body[data-theme="dark"]`:
1083
+ // --bg #131418, --chrome-ink #ecedef, --chrome-ink-dim #a2a3ac,
1084
+ // --accent-solid #b03a0a, --accent-solid-hover #a63308, --accent-fg #fff,
1085
+ // --chrome-danger-ink #ff8787.
1086
+ //
1087
+ // NO BACKTICKS AND NO `${` BELOW except the two interpolations that are
1088
+ // meant: this is a template literal, and the incident class the pre-commit
1089
+ // hook was built for is a stray backtick inside one.
1090
+ const waitPageHtml = (checks) => `<!doctype html><meta charset="utf-8"><title>Summrise</title>
1091
+ <style>
1092
+ body{font-family:system-ui,sans-serif;background:#131418;color:#ecedef;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
1093
+ div{text-align:center;max-width:640px;padding:0 24px}
1094
+ h2{margin:0;font-size:24px;font-weight:600}
1095
+ p{margin:10px 0 0;font-size:15px;color:#a2a3ac}
1096
+ #action{margin-top:12px;font-size:14px}
1097
+ #action.failed{color:#ff8787}
1098
+ button{margin-top:18px;padding:10px 22px;font-size:15px;border-radius:8px;border:0;background:#b03a0a;color:#fff;cursor:pointer}
1099
+ button:hover:not(:disabled){background:#a63308}
1100
+ button:disabled{opacity:.5;cursor:default}
1101
+ </style>
1050
1102
  <div>
1051
- <h2>Summrise Agent is not running</h2>
1052
- <p id="status">waiting for ${(0, url_policy_1.agentBase)().replace("http://", "")}…</p>
1103
+ <h2>The Summrise Agent isn&#39;t answering</h2>
1104
+ <p id="status">no reply from ${(0, url_policy_1.agentBase)().replace("http://", "")}</p>
1105
+ <p id="action" hidden></p>
1053
1106
  <button id="start">Start Agent</button>
1054
1107
  </div>
1055
1108
  <script>
1056
- const btn = document.getElementById("start");
1057
- const st = document.getElementById("status");
1058
- let busy = false;
1059
- btn.addEventListener("click", async () => {
1109
+ var BASE = ${JSON.stringify((0, url_policy_1.agentBase)().replace("http://", ""))};
1110
+ var CTRL = "http://127.0.0.1:9444";
1111
+ var btn = document.getElementById("start");
1112
+ var st = document.getElementById("status");
1113
+ var act = document.getElementById("action");
1114
+ var checks = ${checks};
1115
+ var lastCheckAt = Date.now();
1116
+ function say(el, text, failed) {
1117
+ el.hidden = false;
1118
+ el.textContent = text;
1119
+ el.className = failed ? "failed" : "";
1120
+ }
1121
+ function age(ms) { return ms < 1500 ? "just now" : Math.round(ms / 1000) + "s ago"; }
1122
+ function paint() {
1123
+ st.textContent = "no reply from " + BASE + " \\u00b7 last check " + age(Date.now() - lastCheckAt)
1124
+ + " \\u00b7 " + checks + (checks === 1 ? " check" : " checks");
1125
+ }
1126
+ paint();
1127
+ setInterval(paint, 1000);
1128
+ function poll() {
1129
+ fetch(CTRL + "/api/shell/agent-status").then(function (r) { return r.json(); }).then(function (j) {
1130
+ checks += 1; lastCheckAt = Date.now(); paint();
1131
+ if (j && j.running) { location.replace("http://" + BASE + "/desktop/"); return; }
1132
+ setTimeout(poll, 2000);
1133
+ }).catch(function () {
1134
+ checks += 1; lastCheckAt = Date.now(); paint();
1135
+ setTimeout(poll, 2000);
1136
+ });
1137
+ }
1138
+ setTimeout(poll, 2000);
1139
+ var busy = false;
1140
+ btn.addEventListener("click", async function () {
1060
1141
  if (busy) return;
1061
1142
  busy = true; btn.disabled = true;
1062
- st.textContent = "starting SummriseAgent task…";
1143
+ say(act, "asking the SummriseAgent task to start\\u2026", false);
1063
1144
  try {
1064
- await fetch("http://127.0.0.1:9444/api/shell/start-agent", { method: "POST" });
1065
- st.textContent = "started — waiting for the agent to come up…";
1145
+ var r = await fetch(CTRL + "/api/shell/start-agent", { method: "POST" });
1146
+ var j = null;
1147
+ try { j = await r.json(); } catch (e) { j = null; }
1148
+ if (j && j.ok) {
1149
+ say(act, "the SummriseAgent task was started \\u2014 waiting for it to answer", false);
1150
+ } else {
1151
+ say(act, "the start request FAILED: " + ((j && j.error) || ("HTTP " + r.status)), true);
1152
+ }
1066
1153
  } catch (e) {
1067
- st.textContent = "start request failed (" + e + ") — the task may already be starting";
1154
+ say(act, "the start request did not reach the shell: " + e, true);
1068
1155
  }
1069
- setTimeout(() => { busy = false; btn.disabled = false; }, 3000);
1156
+ setTimeout(function () { busy = false; btn.disabled = false; }, 3000);
1070
1157
  });
1071
1158
  </script>`;
1072
- const loadWaitPage = () => {
1073
- win?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(WAIT_HTML)}`).catch(() => { });
1159
+ const loadWaitPage = (checks) => {
1160
+ // DO NOT RE-LOAD A PAGE THAT IS ALREADY SHOWING (see the note above): the
1161
+ // reload wiped the button's answer and reset the evidence line, which is
1162
+ // what made the only control on this screen unfalsifiable.
1163
+ if (win?.webContents.getURL().startsWith("data:text/html"))
1164
+ return;
1165
+ win?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(waitPageHtml(checks))}`).catch(() => { });
1074
1166
  };
1075
1167
  let retryMs = 2000;
1076
1168
  const nextRetry = () => { retryMs = Math.min(retryMs * 2, 30000); return retryMs; };
@@ -1105,8 +1197,11 @@ if (gotTheLock) {
1105
1197
  win?.loadURL(`${(0, url_policy_1.agentBase)()}/desktop/`).catch(() => { });
1106
1198
  }
1107
1199
  else {
1108
- loadWaitPage();
1200
+ // THE MISS IS COUNTED BEFORE THE PAGE IS HANDED THE NUMBER, because this
1201
+ // failed probe IS one: the count the operator reads is the number of
1202
+ // times the shell has asked and got nothing, including this one.
1109
1203
  agentMissCount += 1;
1204
+ loadWaitPage(agentMissCount);
1110
1205
  if (agentMissCount >= AUTO_START_AFTER_MISSES && Date.now() - lastAutoStartAt > AUTO_START_MIN_GAP_MS) {
1111
1206
  lastAutoStartAt = Date.now();
1112
1207
  agentMissCount = 0;