summrise-agent 1.2.491 → 1.2.493

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;
@@ -310,8 +311,42 @@ exports.psq = psq;
310
311
  * silently reverts to the backing executable (stock `electron.exe`, since the shell runs
311
312
  * unpackaged) with nothing on the device to read that says why.
312
313
  * PowerShell/Windows AppUserModelIDs cannot contain spaces, and this one does not.
314
+ *
315
+ * WHY IT IS NOT `online.saisi.summrise.agent`, WHICH IS WHAT IT WAS CALLED BEFORE —
316
+ * MEASURED on desktop-14rjcr8 (release 1.2.490), ONE VARIABLE AT A TIME:
317
+ *
318
+ * | window's AppUserModelID | shortcut carries it | taskbar draws |
319
+ * | window's AppUserModelID | shortcuts carrying it | taskbar draws |
320
+ * | online.saisi.summrise.agent | both desktops + Start Menu | Electron logo |
321
+ * | online.saisi.summrise.desktop | both desktops + Start Menu | THE SUNRISE |
322
+ * | online.saisi.summrise.desktop | both desktops, Start Menu GONE | Electron logo |
323
+ *
324
+ * That third row is the one that decides where the identity has to live, and it was taken
325
+ * WITHOUT restarting anything: the same running shell, the same AppUserModelID already set,
326
+ * and the icon fell back to the Electron logo the moment the Start Menu shortcut was
327
+ * deleted. So the shortcut this lookup resolves is the **Start Menu** one — a desktop
328
+ * shortcut is not enough, and writing the identity onto `Summrise.lnk` in
329
+ * `%PUBLIC%\Desktop` alone changes nothing. Microsoft's appids.md does say to apply the ID
330
+ * to "all of the shortcuts" when an application has several launch locations, and it lists
331
+ * the Start menu FIRST; the measurement is what says which of them the taskbar actually
332
+ * reads.
333
+ *
334
+ * The other variables were held: the same `IPropertyStore` call read back on every
335
+ * shortcut, the same `icon.ico` (downloaded and RENDERED — it is the orange sunrise), the
336
+ * same shell restarted under the `SummriseDesktop` task, and in each restarted run the
337
+ * running process appended `AUMID-SET-OK` to a log from inside `setAppUserModelId`, so "the
338
+ * ID was set" is measured rather than assumed.
339
+ *
340
+ * WHY THE OLD ID WAS DEAD. Releases up to 1.2.489 set `online.saisi.summrise.agent` on the
341
+ * process while no shortcut anywhere carried it, so the shell resolved that ID against the
342
+ * backing executable — stock `electron.exe` — and kept the result. `shell:AppsFolder` still
343
+ * lists it as `name=[Electron] path=[online.saisi.summrise.agent]`, an app entry named after
344
+ * `electron.exe`. An ID already resolved to the wrong image is not repaired by writing the
345
+ * shortcut afterwards; the association has to be under a name the machine has never seen.
346
+ * Keep this string STABLE from here: changing it again costs the pin and the jump list, so
347
+ * this is a one-time migration, not a knob.
313
348
  */
314
- exports.DESKTOP_AUMID = "online.saisi.summrise.agent";
349
+ exports.DESKTOP_AUMID = "online.saisi.summrise.desktop";
315
350
  /** The two `IPropertyStore` values `Summrise.lnk` must carry, as DATA.
316
351
  *
317
352
  * Deliberately separated from the PowerShell that performs the write, and exported, so the
@@ -442,45 +477,240 @@ public static class SummriseLnk {
442
477
  // exported: unit-tested in test/cli.test.mjs.
443
478
  function deskShortcutRepairPs(scriptsQ, deskDirQ, sink) {
444
479
  const { id, icon } = lnkIdentity(`${deskDirQ}\\icon.ico`);
480
+ // BOTH shortcuts get the identity, and the START MENU one is the load-bearing half:
481
+ // deleting it from a running shell reverted the taskbar to the Electron logo with the ID
482
+ // still set on the process and both desktop shortcuts still carrying it (see DESKTOP_AUMID).
483
+ // The desktop link is kept because it is what a person double-clicks and because
484
+ // appids.md says to apply the ID to "all of the shortcuts" a user can launch from.
485
+ const lnk = (p, tag) => [
486
+ ` try { $dWs${tag} = New-Object -ComObject WScript.Shell; $dSc${tag} = $dWs${tag}.CreateShortcut(${p}); $dSc${tag}.TargetPath = Join-Path $env:SystemRoot 'System32\\WindowsPowerShell\\v1.0\\powershell.exe'; $dSc${tag}.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "' + $dPs1 + '"'; $dSc${tag}.WorkingDirectory = '${deskDirQ}'; $dSc${tag}.IconLocation = $dIco + ',0'; $dSc${tag}.Save(); 'desk: ' + ${p} + ' repointed to electron shell' | ${sink} } catch { ('desk: ' + ${p} + ' repair failed: ' + $_.Exception.Message) | ${sink} }`,
487
+ ` if ($dSet) { try { [SummriseLnk]::Write(${p}, $dId, $dRes); ('desk: ' + ${p} + ' taskbar identity [' + [SummriseLnk]::Read(${p}) + ']') | ${sink} } catch { ('desk: ' + ${p} + ' identity write failed: ' + $_.Exception.Message) | ${sink} } }`,
488
+ ];
445
489
  return [
446
490
  `$dLnk = Join-Path $env:PUBLIC 'Desktop\\Summrise.lnk'`,
491
+ // ALL-USERS Start Menu: the shortcut the shell's app resolver actually reads, and it is
492
+ // CREATED here when absent — a machine that never had one is exactly the machine whose
493
+ // taskbar shows the Electron logo, and no amount of desktop-side repair reaches it.
494
+ // `setup` already requires elevation (HKLM writes, Program Files), so ProgramData is
495
+ // writable on every path that calls this.
496
+ `$dSm = Join-Path $env:ProgramData 'Microsoft\\Windows\\Start Menu\\Programs\\Summrise.lnk'`,
447
497
  `$dIco = '${deskDirQ}\\icon.ico'`,
448
498
  `$dPs1 = '${scriptsQ}\\start-desktop.ps1'`,
449
499
  `$dId = '${id}'`,
450
500
  `$dRes = '${icon}'`,
451
501
  `$dSet = $false`,
452
- `$dNeed = $false`,
453
- `if (Test-Path $dLnk) {`,
454
- // Compiled only when there IS a link to write, so a headless install does not pay for
455
- // it. The `-as [type]` guard keeps a second call in one process from turning a
456
- // "type already exists" throw into a silent skip of the write below.
502
+ `$dShell = (Test-Path $dIco) -and (Test-Path $dPs1)`,
503
+ // Compiled when there is a SHELL to point at, not when a desktop link happens to exist:
504
+ // the Start Menu shortcut is created from nothing, so gating this on `$dLnk` would skip
505
+ // the identity on precisely the installs that need it. The `-as [type]` guard keeps a
506
+ // second call in one process from turning a "type already exists" throw into a silent
507
+ // skip of every write below.
508
+ `if ($dShell) {`,
457
509
  ` $dCs = @'\n${LNK_PROPS_CS}\n'@`,
458
510
  ` try { if (-not ('SummriseLnk' -as [type])) { Add-Type -TypeDefinition $dCs }; $dSet = [bool]('SummriseLnk' -as [type]) } catch { ('desk: lnk identity helper unavailable: ' + $_.Exception.Message) | ${sink} }`,
511
+ `}`,
512
+ `$dNeed = $false`,
513
+ `if (Test-Path $dLnk) {`,
459
514
  ` try { $dEx = (New-Object -ComObject WScript.Shell).CreateShortcut($dLnk); if (($dEx.TargetPath -like '*summrise-desktop.exe') -or ($dEx.TargetPath -like '*summrise-tray.exe') -or (-not (Test-Path $dEx.TargetPath))) { $dNeed = $true } } catch { $dNeed = $true }`,
460
515
  // ...and a link that is otherwise healthy still needs repairing when its identity is
461
516
  // missing or names a different ID/icon. `Read` returns "id|icon", so ONE comparison
462
517
  // covers both properties — and it is the same call the verification below prints.
463
518
  ` if ($dSet) { try { if (([SummriseLnk]::Read($dLnk)) -ne ($dId + '|' + $dRes)) { $dNeed = $true } } catch { $dNeed = $true } }`,
464
519
  `}`,
465
- `if ($dNeed -and (Test-Path $dIco) -and (Test-Path $dPs1)) {`,
466
- ` try { $dWs = New-Object -ComObject WScript.Shell; $dSc = $dWs.CreateShortcut($dLnk); $dSc.TargetPath = Join-Path $env:SystemRoot 'System32\\WindowsPowerShell\\v1.0\\powershell.exe'; $dSc.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "' + $dPs1 + '"'; $dSc.WorkingDirectory = '${deskDirQ}'; $dSc.IconLocation = $dIco + ',0'; $dSc.Save(); 'desk: Summrise.lnk repointed to electron shell' | ${sink} } catch { ('desk: Summrise.lnk repair failed: ' + $_.Exception.Message) | ${sink} }`,
467
- // The verification is the READ-BACK, not the absence of an exception: SetValue+Commit
468
- // returning cleanly is a claim about a call, and this records what the file now carries.
469
- ` if ($dSet) { try { [SummriseLnk]::Write($dLnk, $dId, $dRes); ('desk: Summrise.lnk taskbar identity [' + [SummriseLnk]::Read($dLnk) + ']') | ${sink} } catch { ('desk: Summrise.lnk identity write failed: ' + $_.Exception.Message) | ${sink} } }`,
520
+ // The Start Menu link is created when ABSENT and rewritten when its identity is wrong —
521
+ // it is never deleted, and a link that already matches is left alone so a second `setup`
522
+ // does not churn the file.
523
+ `$dSmNeed = $false`,
524
+ `if ($dShell) {`,
525
+ ` if (-not (Test-Path $dSm)) { $dSmNeed = $true }`,
526
+ ` elseif (-not $dSet) { $dSmNeed = $true }`,
527
+ ` else { try { if (([SummriseLnk]::Read($dSm)) -ne ($dId + '|' + $dRes)) { $dSmNeed = $true } } catch { $dSmNeed = $true } }`,
528
+ `}`,
529
+ `if ($dNeed -and $dShell) {`,
530
+ ...lnk("$dLnk", "1"),
531
+ `}`,
532
+ `if ($dSmNeed -and $dShell) {`,
533
+ ...lnk("$dSm", "2"),
470
534
  `}`,
471
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} } } }`,
472
536
  ];
473
537
  }
474
- // exported: the start-desktop.ps1 launcher (the SummriseDesktop onlogon task +
475
- // desktop Summrise.lnk both call it). Launches the Electron shell from
476
- // components\summrise-desktop-electron\ with the working directory set there
477
- // (electron . resolves src/main.js via package.json main). The script is
478
- // intentionally tiny + ASCII-only (system-locale PS). unit-tested.
479
- 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) {
480
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.",
481
622
  `$dir = '${deskDirQ}'`,
482
- `Set-Location $dir`,
483
- `& "$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"`,
484
714
  ];
485
715
  }
486
716
  /** The desktop shell's watchdog pair + its task, as ONE PowerShell script.
@@ -492,20 +722,52 @@ function startDesktopPs(deskDirQ) {
492
722
  * never ran setup can still get a working task by asking for the window. Idempotent:
493
723
  * ensure-desktop.ps1 exits when electron is already alive, so the 5-minute trigger never
494
724
  * steals focus. unit-tested. */
495
- function desktopTaskPs(installQ) {
725
+ function desktopTaskPs(installQ, logsQ) {
496
726
  return [
497
727
  "# written by `summrise setup` / `summrise desktop` -- the desktop shell's task + watchdog.",
498
728
  "$ErrorActionPreference = 'Stop'",
499
729
  `$q = '${installQ}'`,
500
730
  `$en = Join-Path $q 'scripts\\ensure-desktop.ps1'`,
501
731
  `$vb = Join-Path $q 'scripts\\desktop-pulse.vbs'`,
502
- `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`,
503
742
  `Set-Content -Path $vb -Value ('CreateObject("WScript.Shell").Run "powershell -NoProfile -ExecutionPolicy Bypass -File " & Chr(34) & "' + $en + '" & Chr(34), 0, False') -Force`,
504
743
  `$da = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument ('"' + $vb + '"') -WorkingDirectory $q`,
505
744
  `$dt1 = New-ScheduledTaskTrigger -AtLogOn`,
506
745
  `$dw1 = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(3) -RepetitionInterval (New-TimeSpan -Minutes 5)`,
507
746
  `$pr = New-ScheduledTaskPrincipal -UserId ('{0}\\{1}' -f $env:USERDOMAIN, $env:USERNAME) -LogonType Interactive -RunLevel Highest`,
508
- `$st = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 10) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew`,
747
+ // NO EXECUTION TIME LIMIT -- the same value, in the same spelling, the agent's own task has
748
+ // carried since round 118 for the same reason: `- ExecutionTimeLimit 0 never kill the
749
+ // running task`. It was `-Minutes 10` here, and that WAS the defect.
750
+ //
751
+ // WHY A LIMIT KILLS A SHELL MEANT TO LIVE INDEFINITELY: this task's action is
752
+ // desktop-pulse.vbs -> ensure-desktop.ps1 -> start-desktop.ps1, and that last line is
753
+ // `& electron.exe .` -- the PowerShell CALL OPERATOR, which WAITS. The PowerShell the task
754
+ // launched therefore stays alive for as long as the shell does, as the shell's PARENT, and
755
+ // Task Scheduler enforces ExecutionTimeLimit on that process tree. Ten minutes after every
756
+ // start the scheduler killed the running shell; five minutes later the guarded pulse
757
+ // correctly brought a fresh one back; ten minutes after that it died again. The operator
758
+ // reported that as "反复重启", four times.
759
+ //
760
+ // NOTHING RELIED ON THE KILL, which is why removing it is the fix rather than a trade. This
761
+ // repository's recovery for a shell that is GONE is the guarded 5-minute pulse, and
762
+ // summrise-desktop-electron/src/main.ts says so from the other side, naming a logon-only task
763
+ // as one that "cannot recover a wedged shell". A shell that is RUNNING but wedged was never
764
+ // this limit's business either: the pulse's guard is a process-EXISTENCE check
765
+ // (`if (Get-Process electron) { exit }`), so it cannot see a wedged shell whether the limit is
766
+ // ten minutes or absent. `summrise update` is what heals a wedged shell, and its log says so.
767
+ //
768
+ // Measured on desktop-14rjcr8: under PT10M the four electron PIDs were replaced about every
769
+ // ten minutes; under PT0S the same four PIDs were still there 11 minutes later.
770
+ `$st = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Seconds 0) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew`,
509
771
  `Register-ScheduledTask SummriseDesktop -Action $da -Trigger @($dt1,$dw1) -Principal $pr -Settings $st -Force | Out-Null`,
510
772
  `Start-ScheduledTask -TaskName SummriseDesktop`,
511
773
  ];
@@ -2206,7 +2468,7 @@ const commands = {
2206
2468
  // repetition is what reborns a dead shell.
2207
2469
  try {
2208
2470
  const reg = path.join(SCRIPTS_DIR, "register-desktop-task.ps1");
2209
- 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");
2210
2472
  // THE STATUS WAS DISCARDED AND THE SUCCESS LINE WAS UNCONDITIONAL, which is the defect the AGENT
2211
2473
  // task's check ten lines below names in its own comment: "audit #7: used to claim success
2212
2474
  // regardless". Worse, the `catch` below could not fire for this — `sh()` RETURNS, it does not
@@ -2237,7 +2499,7 @@ const commands = {
2237
2499
  // SummriseDesktop onlogon task + desktop Summrise.lnk both call it). Was never
2238
2500
  // written before — a real gap that left the shell unlaunchable.
2239
2501
  fs.mkdirSync(SCRIPTS_DIR, { recursive: true });
2240
- 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");
2241
2503
  console.log("setup: scripts\\start-desktop.ps1 written");
2242
2504
  // Register boot-start task (SYSTEM) and kick it once; the agent's own
2243
2505
  // first-run flow registers the device with the console using the key.
@@ -2474,7 +2736,7 @@ const commands = {
2474
2736
  if (!fs.existsSync(reg)) {
2475
2737
  // A machine that never ran setup still gets a working task: the same script setup
2476
2738
  // writes, from the same builder, so the two cannot drift.
2477
- 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");
2478
2740
  }
2479
2741
  // THE START SCRIPT GOES IN A FILE, AND ONLY IT DECIDES WHETHER TO REGISTER. Spawning with
2480
2742
  // `shell: true` means cmd.exe, and **cmd splits a command on `&`** — the PowerShell call
@@ -2924,7 +3186,7 @@ const commands = {
2924
3186
  // the swap script's migration, and a headless install simply never
2925
3187
  // calls the launcher (shortcut repair is Test-Path guarded anyway).
2926
3188
  fs.mkdirSync(SCRIPTS_DIR, { recursive: true });
2927
- 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");
2928
3190
  // round-298: record the release version on a PROVABLY successful swap
2929
3191
  // so agent_update (which reads <install>/.summrise-release as its local
2930
3192
  // version) reports up_to_date instead of re-swapping every call.
@@ -3015,13 +3277,29 @@ const commands = {
3015
3277
  // wscript wrapper runs it with no console flash.
3016
3278
  `$en1 = '${q}\\scripts\\ensure-desktop.ps1'`,
3017
3279
  `$vb1 = '${q}\\scripts\\desktop-pulse.vbs'`,
3018
- `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`,
3019
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`,
3020
3290
  `if ($null -ne (Get-ScheduledTask -TaskName 'SummriseDesktop' -ErrorAction SilentlyContinue)) {`,
3021
3291
  ` $da = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument ('"' + $vb1 + '"') -WorkingDirectory '${q}'`,
3022
3292
  ` $dt1 = New-ScheduledTaskTrigger -AtLogOn`,
3023
3293
  ` $dw1 = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(3) -RepetitionInterval (New-TimeSpan -Minutes 5)`,
3024
- ` Set-ScheduledTask -TaskName 'SummriseDesktop' -Action $da -Trigger @($dt1, $dw1) | Out-Null`,
3294
+ // AND THE SETTINGS, WHICH IS THE HALF THAT REACHES A MACHINE ALREADY INSTALLED.
3295
+ // This call used to pass only -Action and -Trigger, and `Set-ScheduledTask` PRESERVES the
3296
+ // components it is not given — so an execution limit already on the task survived every
3297
+ // update. That is the wrong shape for a heal: `summrise update` is the only mechanism that
3298
+ // reaches the fleet, and a device carrying the old 10-minute limit would have kept it
3299
+ // forever while its log said "hardened". The value is the same one `setup` writes, so the
3300
+ // two cannot diverge — and installer_integrity.rs holds both ends to it.
3301
+ ` $dst = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Seconds 0) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew`,
3302
+ ` Set-ScheduledTask -TaskName 'SummriseDesktop' -Action $da -Trigger @($dt1, $dw1) -Settings $dst | Out-Null`,
3025
3303
  ` "[$(Get-Date -Format o)] desk: SummriseDesktop hardened (guarded 5-min pulse)" | ${log}`,
3026
3304
  `}`,
3027
3305
  `if ((Test-Path $deskDir) -and (Test-Path $deskStart)) {`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summrise-agent",
3
- "version": "1.2.491",
3
+ "version": "1.2.493",
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
@@ -909,10 +909,17 @@ if (gotTheLock) {
909
909
  // there) as System.AppUserModel.ID — the same string — plus RelaunchIconResource pointing
910
910
  // at the sunrise .ico. KEEP THE TWO IN STEP; the literal below is a copy because this file
911
911
  // is emitted to plain JS in two packages and cannot import the CLI's module.
912
+ // THE VALUE IS NOT THE ONE THIS FILE USED BEFORE ("…summrise.agent"), and that is the
913
+ // measured half of the fix rather than a rename. On desktop-14rjcr8, with that string set
914
+ // on the window AND read back off the shortcut, the taskbar still drew the Electron logo:
915
+ // releases up to 1.2.489 had already resolved that AUMID — set with no shortcut anywhere —
916
+ // against the backing executable and kept the answer, and `shell:AppsFolder` still lists it
917
+ // as an app named "Electron". The same machine drew the SUNRISE under a string it had never
918
+ // seen, with every other variable held. DESKTOP_AUMID carries the two-row table.
912
919
  // (The same shortcut association is also the documented precondition for the hide-to-tray
913
920
  // toast below: Electron requires a Start Menu shortcut carrying the ID and a
914
921
  // ToastActivatorCLSID, and this one carries neither — unchanged by this call.)
915
- const AUMID = "online.saisi.summrise.agent";
922
+ const AUMID = "online.saisi.summrise.desktop";
916
923
  try {
917
924
  if (process.platform === "win32")
918
925
  electron_1.app.setAppUserModelId(AUMID);