instar 1.3.1600 → 1.3.1602

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/commands/init.d.ts +7 -0
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +8 -1
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/server.d.ts.map +1 -1
  6. package/dist/commands/server.js +65 -0
  7. package/dist/commands/server.js.map +1 -1
  8. package/dist/core/AppConversationService.d.ts +39 -1
  9. package/dist/core/AppConversationService.d.ts.map +1 -1
  10. package/dist/core/AppConversationService.js +137 -2
  11. package/dist/core/AppConversationService.js.map +1 -1
  12. package/dist/core/AppConversationStore.d.ts +2 -0
  13. package/dist/core/AppConversationStore.d.ts.map +1 -1
  14. package/dist/core/AppConversationStore.js +4 -0
  15. package/dist/core/AppConversationStore.js.map +1 -1
  16. package/dist/core/AppMediaReplicator.d.ts +241 -0
  17. package/dist/core/AppMediaReplicator.d.ts.map +1 -0
  18. package/dist/core/AppMediaReplicator.js +903 -0
  19. package/dist/core/AppMediaReplicator.js.map +1 -0
  20. package/dist/core/AppMediaStore.d.ts +28 -0
  21. package/dist/core/AppMediaStore.d.ts.map +1 -1
  22. package/dist/core/AppMediaStore.js +87 -13
  23. package/dist/core/AppMediaStore.js.map +1 -1
  24. package/dist/core/BrowserCareProbes.d.ts +15 -0
  25. package/dist/core/BrowserCareProbes.d.ts.map +1 -0
  26. package/dist/core/BrowserCareProbes.js +78 -0
  27. package/dist/core/BrowserCareProbes.js.map +1 -0
  28. package/dist/core/MachineLockStateReader.d.ts +34 -0
  29. package/dist/core/MachineLockStateReader.d.ts.map +1 -0
  30. package/dist/core/MachineLockStateReader.js +114 -0
  31. package/dist/core/MachineLockStateReader.js.map +1 -0
  32. package/dist/core/MeshRpc.d.ts +1 -1
  33. package/dist/core/MeshRpc.d.ts.map +1 -1
  34. package/dist/core/MeshRpc.js +4 -0
  35. package/dist/core/MeshRpc.js.map +1 -1
  36. package/dist/core/PostUpdateMigrator.d.ts +14 -0
  37. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  38. package/dist/core/PostUpdateMigrator.js +60 -2
  39. package/dist/core/PostUpdateMigrator.js.map +1 -1
  40. package/dist/core/WindowRunLivenessAuthority.d.ts.map +1 -1
  41. package/dist/core/WindowRunLivenessAuthority.js +11 -1
  42. package/dist/core/WindowRunLivenessAuthority.js.map +1 -1
  43. package/dist/core/appConversationText.d.ts +5 -0
  44. package/dist/core/appConversationText.d.ts.map +1 -1
  45. package/dist/core/appConversationText.js +17 -2
  46. package/dist/core/appConversationText.js.map +1 -1
  47. package/dist/core/appInboundDispatch.d.ts +14 -0
  48. package/dist/core/appInboundDispatch.d.ts.map +1 -1
  49. package/dist/core/appInboundDispatch.js +25 -0
  50. package/dist/core/appInboundDispatch.js.map +1 -1
  51. package/dist/core/types.d.ts +10 -0
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/data/standards-guard-index.json +1 -1
  55. package/dist/data/standards-guard-index.meta.json +2 -2
  56. package/dist/data/standards-registry.meta.json +1 -1
  57. package/dist/monitoring/machineMaintenance/BrowserCare.d.ts +166 -0
  58. package/dist/monitoring/machineMaintenance/BrowserCare.d.ts.map +1 -0
  59. package/dist/monitoring/machineMaintenance/BrowserCare.js +334 -0
  60. package/dist/monitoring/machineMaintenance/BrowserCare.js.map +1 -0
  61. package/dist/monitoring/machineMaintenance/lockState.d.ts +34 -0
  62. package/dist/monitoring/machineMaintenance/lockState.d.ts.map +1 -0
  63. package/dist/monitoring/machineMaintenance/lockState.js +53 -0
  64. package/dist/monitoring/machineMaintenance/lockState.js.map +1 -0
  65. package/dist/monitoring/machineMaintenance/readings.d.ts +4 -0
  66. package/dist/monitoring/machineMaintenance/readings.d.ts.map +1 -1
  67. package/dist/monitoring/machineMaintenance/readings.js +2 -1
  68. package/dist/monitoring/machineMaintenance/readings.js.map +1 -1
  69. package/dist/native/instar-desktop-host +0 -0
  70. package/dist/scaffold/templates.d.ts.map +1 -1
  71. package/dist/scaffold/templates.js +2 -1
  72. package/dist/scaffold/templates.js.map +1 -1
  73. package/dist/server/AgentServer.d.ts.map +1 -1
  74. package/dist/server/AgentServer.js +8 -1
  75. package/dist/server/AgentServer.js.map +1 -1
  76. package/dist/server/CapabilityIndex.js +3 -3
  77. package/dist/server/CapabilityIndex.js.map +1 -1
  78. package/dist/server/mobileConversationRoutes.d.ts +48 -0
  79. package/dist/server/mobileConversationRoutes.d.ts.map +1 -1
  80. package/dist/server/mobileConversationRoutes.js +259 -33
  81. package/dist/server/mobileConversationRoutes.js.map +1 -1
  82. package/dist/server/mobileMediaRoutes.d.ts.map +1 -1
  83. package/dist/server/mobileMediaRoutes.js +11 -1
  84. package/dist/server/mobileMediaRoutes.js.map +1 -1
  85. package/dist/server/mobilePairingRoutes.d.ts.map +1 -1
  86. package/dist/server/mobilePairingRoutes.js +34 -12
  87. package/dist/server/mobilePairingRoutes.js.map +1 -1
  88. package/dist/server/routes.d.ts.map +1 -1
  89. package/dist/server/routes.js +47 -3
  90. package/dist/server/routes.js.map +1 -1
  91. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  92. package/dist/testing/selfActionRegistry.js +41 -0
  93. package/dist/testing/selfActionRegistry.js.map +1 -1
  94. package/package.json +1 -1
  95. package/scripts/blocking-decision-surfaces-baseline.json +22 -2
  96. package/src/data/builtin-manifest.json +64 -64
  97. package/src/data/standards-guard-index.json +1 -1
  98. package/src/data/standards-guard-index.meta.json +2 -2
  99. package/src/data/standards-registry.meta.json +1 -1
  100. package/src/data/state-coherence-registry.json +37 -0
  101. package/src/scaffold/templates.ts +2 -1
  102. package/src/templates/scripts/browser/chrome-close.sh +97 -0
  103. package/src/templates/scripts/browser/chrome-js.sh +2 -0
  104. package/src/templates/scripts/browser/chrome-launch.sh +16 -3
  105. package/src/templates/scripts/browser/chrome-ledger.sh +49 -0
  106. package/src/templates/scripts/browser/chrome-renew.sh +38 -0
  107. package/src/templates/scripts/browser/chrome-unsaved.sh +83 -0
  108. package/upgrades/1.3.1601.md +68 -0
  109. package/upgrades/1.3.1602.md +24 -0
  110. package/upgrades/side-effects/machine-self-maintenance-browser-care-lock-reading.md +98 -0
  111. package/upgrades/side-effects/mobile-native-chat-media-cross-machine.md +131 -0
  112. package/upgrades/side-effects/native-conversations-gaps.md +129 -0
  113. package/upgrades/side-effects/window-run-liveness-later-loss.md +81 -0
  114. package/upgrades/1.3.1600.md +0 -34
@@ -931,6 +931,43 @@
931
931
  "keepSegments": 4
932
932
  }
933
933
  },
934
+ {
935
+ "category": "browser-instance-ledger",
936
+ "description": "Browser care (docs/specs/machine-self-maintenance.md, Part B): one small file per normal-Chrome instance this agent opened (profile dir, pid + start time, session, purpose, lease). Written by the browser helper scripts and the watch-only sweep; describes live processes on this machine only.",
937
+ "scope": "machine-local",
938
+ "freshness": "live",
939
+ "conflictShape": "single-writer",
940
+ "transport": "none",
941
+ "paths": [
942
+ "state/browser-instances/"
943
+ ],
944
+ "grandfathered": false,
945
+ "retention": {
946
+ "class": "R",
947
+ "access": "protocol-reader",
948
+ "boundedBy": "domain-cardinality",
949
+ "note": "One file per running owned Chrome instance; BrowserCareSweep removes the file once its process has exited, so the count tracks open browsers, never history."
950
+ }
951
+ },
952
+ {
953
+ "category": "audit-browser-care",
954
+ "description": "Browser care audit (docs/specs/machine-self-maintenance.md, Part B): what the watch-only sweep and chrome-close.sh would close or keep, and why. Profile paths relative to the agent home, never page content. Machine-local observability; never authority, never replicated.",
955
+ "scope": "machine-local",
956
+ "freshness": "n/a",
957
+ "conflictShape": "append-only",
958
+ "transport": "none",
959
+ "paths": [
960
+ "logs/browser-care.jsonl"
961
+ ],
962
+ "grandfathered": false,
963
+ "retention": {
964
+ "class": "A",
965
+ "access": "streamed",
966
+ "maxBytes": 2097152,
967
+ "keepSegments": 1,
968
+ "note": "BrowserCareSweep.audit() rotates the file to .1 (one kept segment) when it passes 2 MiB before appending; it writes only when an instance's verdict changes."
969
+ }
970
+ },
934
971
  {
935
972
  "category": "audit-machine-drain",
936
973
  "description": "Watch-only drain audit (docs/specs/machine-self-maintenance.md): what each nightly rehearsal would have done, phase by phase, with delays, holds and yield times. Machine-local observability of this machine; never authority, never replicated.",
@@ -20,7 +20,7 @@ import { JOB_DECLARED_FAILURE_AWARENESS } from "../scheduler/jobDeclaredFailure.
20
20
  // existing-agent migration there) so the two can never drift. Imported as a runtime
21
21
  // function call inside generateClaudeMd — no module-init cycle (PostUpdateMigrator
22
22
  // never imports templates).
23
- import { APPRENTICESHIP_STALL_GATE_CLAUDEMD_SECTION, SESSION_LISTING_HYGIENE_CLAUDEMD_SECTION, PLAYWRIGHT_PROFILE_REGISTRY_CLAUDEMD_SECTION, MACHINE_LOAD_ASSESSMENT_CLAUDEMD_SECTION, CODEX_CONNECT_CLAUDEMD_SECTION, FEATURE_SWITCH_CLAUDEMD_SECTION, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION, SINGLE_MACHINE_FAILOVER_GAP_CLAUDEMD_SECTION, MISSING_LOGIN_SESSION_CLAUDEMD_SECTION, DYNAMIC_MCP_CLAUDEMD_SECTION, ULTRACODE_SPAWN_CLAUDEMD_SECTION, AGENT_SIGNATURE_PROVENANCE_CLAUDEMD_SECTION, SENDER_REJECTION_CLAUDEMD_SECTION, SCOPE_ACCRETION_CLAUDEMD_SECTION, MESH_SELF_HEALING_CLAUDEMD_SECTION, LEASE_MEDIUM_CLAUDEMD_SECTION, WRITE_ADMISSION_CLAUDEMD_SECTION, DOORWAY_REGISTRY_CLAUDEMD_SECTION, EXTERNAL_HOG_CLAUDEMD_SECTION, ROUTING_SPEND_CLAUDEMD_SECTION, MATURATION_BOARD_CLAUDEMD_SECTION, QUEUE_PACE_CLAUDEMD_SECTION, ECOSYSTEM_IDEAS_CLAUDEMD_SECTION, MOBILE_PAIRING_CLAUDEMD_SECTION, MOBILE_CHAT_CLAUDEMD_SECTION, APP_CONVERSATIONS_CLAUDEMD_SECTION, MOBILE_ROUTES_CLAUDEMD_SECTION, MOBILE_PUSH_CLAUDEMD_SECTION, DECISION_QUALITY_CLAUDEMD_SECTION, BENCHMARK_DIVERGENCE_CLAUDEMD_SECTION, DUPLICATE_RECONCILER_CLAUDEMD_SECTION, AUDIT_CONVERGENCE_CLAUDEMD_SECTION, TONE_ADVISORY_MIGRATION_CLAUDEMD_SECTION, DECISION_JOURNAL_CONFIDENCE_CLAUDEMD_GUIDANCE, WINDOW_RUN_LIVENESS_CLAUDEMD_SECTION, SCRIPT_JOBS_CLAUDEMD_SECTION, JOB_RESULT_SUPERVISION_CLAUDEMD_SECTION, DEDICATED_PROFILE_REGISTRY_ROW_ANSWER, CODEX_ARM_SWEEPER_CLAUDEMD_SECTION, MACHINE_MAINTENANCE_CLAUDEMD_SECTION, APP_MEDIA_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
23
+ import { APPRENTICESHIP_STALL_GATE_CLAUDEMD_SECTION, SESSION_LISTING_HYGIENE_CLAUDEMD_SECTION, PLAYWRIGHT_PROFILE_REGISTRY_CLAUDEMD_SECTION, MACHINE_LOAD_ASSESSMENT_CLAUDEMD_SECTION, CODEX_CONNECT_CLAUDEMD_SECTION, FEATURE_SWITCH_CLAUDEMD_SECTION, NORMAL_CHROME_BROWSER_CLAUDEMD_SECTION, SINGLE_MACHINE_FAILOVER_GAP_CLAUDEMD_SECTION, MISSING_LOGIN_SESSION_CLAUDEMD_SECTION, DYNAMIC_MCP_CLAUDEMD_SECTION, ULTRACODE_SPAWN_CLAUDEMD_SECTION, AGENT_SIGNATURE_PROVENANCE_CLAUDEMD_SECTION, SENDER_REJECTION_CLAUDEMD_SECTION, SCOPE_ACCRETION_CLAUDEMD_SECTION, MESH_SELF_HEALING_CLAUDEMD_SECTION, LEASE_MEDIUM_CLAUDEMD_SECTION, WRITE_ADMISSION_CLAUDEMD_SECTION, DOORWAY_REGISTRY_CLAUDEMD_SECTION, EXTERNAL_HOG_CLAUDEMD_SECTION, ROUTING_SPEND_CLAUDEMD_SECTION, MATURATION_BOARD_CLAUDEMD_SECTION, QUEUE_PACE_CLAUDEMD_SECTION, ECOSYSTEM_IDEAS_CLAUDEMD_SECTION, MOBILE_PAIRING_CLAUDEMD_SECTION, MOBILE_CHAT_CLAUDEMD_SECTION, APP_CONVERSATIONS_CLAUDEMD_SECTION, MOBILE_ROUTES_CLAUDEMD_SECTION, MOBILE_PUSH_CLAUDEMD_SECTION, DECISION_QUALITY_CLAUDEMD_SECTION, BENCHMARK_DIVERGENCE_CLAUDEMD_SECTION, DUPLICATE_RECONCILER_CLAUDEMD_SECTION, AUDIT_CONVERGENCE_CLAUDEMD_SECTION, TONE_ADVISORY_MIGRATION_CLAUDEMD_SECTION, DECISION_JOURNAL_CONFIDENCE_CLAUDEMD_GUIDANCE, WINDOW_RUN_LIVENESS_CLAUDEMD_SECTION, SCRIPT_JOBS_CLAUDEMD_SECTION, JOB_RESULT_SUPERVISION_CLAUDEMD_SECTION, DEDICATED_PROFILE_REGISTRY_ROW_ANSWER, CODEX_ARM_SWEEPER_CLAUDEMD_SECTION, MACHINE_MAINTENANCE_CLAUDEMD_SECTION, APP_MEDIA_CLAUDEMD_SECTION, MACHINE_BROWSER_CARE_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
24
24
  import { MINIMUM_OPERATOR_EFFORT_CLAUDEMD_SECTION } from '../core/PostUpdateMigrator.js';
25
25
  import { CODEX_DEVICE_LIMIT_CLAUDEMD_BULLET } from '../core/PostUpdateMigrator.js';
26
26
  import { UPDATE_APPLY_VERIFY_HOLD_CLAUDEMD_LINE } from '../core/PostUpdateMigrator.js';
@@ -457,6 +457,7 @@ ${INTERACTIVE_LOGIN_EXPIRED_BULLET}
457
457
  ${CODEX_ARM_SWEEPER_CLAUDEMD_SECTION(port)}
458
458
 
459
459
  ${MACHINE_MAINTENANCE_CLAUDEMD_SECTION(port)}
460
+ ${MACHINE_BROWSER_CARE_CLAUDEMD_SECTION(port)}
460
461
 
461
462
  ### Cold-Start Lifeline Fallback (no silent resource rejection)
462
463
 
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ # chrome-close.sh — say you are done with one profile's normal Chrome (machine self-maintenance, Part B).
3
+ # Usage: chrome-close.sh <profile-id|/absolute/user-data-dir> [--when-free]
4
+ # DRY-RUN in this release: it closes nothing. It records that the task is finished (the ledger's
5
+ # completion signal, which is what lets the cleanup sweep close the browser once closing is turned
6
+ # on), runs the same read-only unsaved-input and download checks a real close would, and prints and
7
+ # logs what a close would do. --when-free records the completion signal only, for a browser you
8
+ # cannot close yourself. If it reports unsaved input, submit or clear the form first.
9
+ # macOS only (Apple Events). Installed by instar into .instar/scripts/browser/.
10
+ if [ "$(uname -s)" != "Darwin" ]; then
11
+ echo "ERR macos-only: the normal-Chrome method uses macOS Apple Events; it does not run on $(uname -s)" >&2
12
+ exit 2
13
+ fi
14
+ [ -z "$1" ] && { echo "usage: chrome-close.sh <profile-id|/absolute/user-data-dir> [--when-free]" >&2; exit 64; }
15
+ WHENFREE=0; [ "$2" = "--when-free" ] && WHENFREE=1
16
+ # Resolve the profile to a canonical user-data-dir STRICTLY INSIDE this agent's home (the same
17
+ # jail the Playwright profile registry applies). This guards against mistakes, not against a
18
+ # hostile caller with a shell: it keeps an ordinary call from reaching the operator's own Chrome. realpath resolves `..` and symlinks before the check, and the canonical
19
+ # path is what every later step uses (mkdir, Preferences, open, the process match).
20
+ AGENT_HOME="$(cd "$(dirname "$0")/../../.." && pwd -P)"
21
+ case "$AGENT_HOME" in *[[:space:]]*) echo "ERR agent-home-has-spaces: $AGENT_HOME (the exact process match needs a path without spaces; these tools cannot be used from this agent home)" >&2; exit 64 ;; esac
22
+ case "$1" in
23
+ /*) RAW="$1" ;;
24
+ */*|.*) echo "ERR bad-profile-id: use a profile id from GET /playwright-profiles or an absolute directory inside this agent's home" >&2; exit 64 ;;
25
+ *) printf '%s' "$1" | grep -Eq '^[a-z0-9-]{1,64}$' || { echo "ERR bad-profile-id: ids are lowercase letters, digits and dashes (the profile registry's rule)" >&2; exit 64; }
26
+ RAW="$AGENT_HOME/.instar/state/playwright-profiles/$1" ;;
27
+ esac
28
+ case "$RAW" in *[[:space:]]*) echo "ERR bad-profile-path: no spaces or tabs in a profile path (the process match could pick another profile)" >&2; exit 64 ;; esac
29
+ P="$(/usr/bin/python3 -I -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$RAW")" || { echo "ERR profile-resolve-failed: $RAW" >&2; exit 64; }
30
+ case "$P" in
31
+ "$AGENT_HOME"/?*) ;;
32
+ *) echo "ERR profile-outside-agent-home: $RAW (browser profiles must live under $AGENT_HOME)" >&2; exit 64 ;;
33
+ esac
34
+ # Inside the home, the agent's own folders are off limits except the profiles folder, and the
35
+ # operator's own Chrome folder is always refused (in case an agent home were ever ~).
36
+ case "$P/" in
37
+ "$HOME/Library/Application Support/Google/Chrome/"*) echo "ERR profile-reserved-folder: $P is the operator's own Chrome" >&2; exit 64 ;;
38
+ "$AGENT_HOME/.instar/state/playwright-profiles/"?*) ;;
39
+ "$AGENT_HOME/.instar/"*|"$AGENT_HOME/.claude/"*) echo "ERR profile-reserved-folder: $P is an agent config folder, not a browser profile" >&2; exit 64 ;;
40
+ esac
41
+ case "$P" in *[[:space:]]*) echo "ERR bad-profile-path: $P resolves to a path with spaces or tabs" >&2; exit 64 ;; esac
42
+ D="$(dirname "$0")"
43
+ "$D/chrome-ledger.sh" complete "$P"
44
+ LOG="$AGENT_HOME/logs/browser-care.jsonl"
45
+ log() { { mkdir -p "$(dirname "$LOG")" && HOMEDIR="$AGENT_HOME" PROFILE="$P" EVENT="$1" REASON="$2" /usr/bin/python3 -I -c '
46
+ import json, os, time
47
+ print(json.dumps({"ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "event": os.environ["EVENT"], "dryRun": True,
48
+ "source": "chrome-close.sh", "profile": os.path.relpath(os.environ["PROFILE"], os.environ["HOMEDIR"]),
49
+ "reason": os.environ["REASON"] or None}))' >> "$LOG"; } 2>/dev/null || echo "warning: could not write $LOG" >&2; }
50
+ if [ "$WHENFREE" = 1 ]; then
51
+ log completion-recorded ""
52
+ echo "recorded: finished with $1 (the cleanup sweep may close it once closing is turned on)"
53
+ exit 0
54
+ fi
55
+ PID=$(ps -axo pid=,args= | K="--user-data-dir=$P" awk '/Google Chrome\.app\/Contents\/MacOS\/Google Chrome / && index($0" ", ENVIRON["K"]" ") && !/--type=/ && !/--remote-debugging-|--enable-automation|--test-type/ { print $1; exit }')
56
+ [ -z "$PID" ] && { log not-running ""; echo "not running"; exit 0; }
57
+ # A download still growing, then every tab: the checks a real close runs first.
58
+ DL="$(PROFILE="$P" /usr/bin/python3 -I -c '
59
+ import json, os, time
60
+ p = os.environ["PROFILE"]
61
+ d = os.path.join(os.path.expanduser("~"), "Downloads")
62
+ try:
63
+ with open(os.path.join(p, "Default", "Preferences")) as h:
64
+ x = json.load(h).get("download", {}).get("default_directory")
65
+ if isinstance(x, str) and os.path.isabs(x): d = x
66
+ except Exception: pass
67
+ try:
68
+ names = os.listdir(d)
69
+ except FileNotFoundError:
70
+ print("no"); raise SystemExit
71
+ except Exception:
72
+ print("unreadable"); raise SystemExit
73
+ now = time.time()
74
+ for n in names:
75
+ if n.endswith(".crdownload"):
76
+ try:
77
+ if now - os.stat(os.path.join(d, n)).st_mtime < 300: print("yes"); raise SystemExit
78
+ except OSError:
79
+ print("unreadable"); raise SystemExit
80
+ print("no")')"
81
+ case "$DL" in
82
+ yes) log would-keep download-active; echo "DRY-RUN: would KEEP: a download is still growing (nothing closed)"; exit 0 ;;
83
+ no) ;;
84
+ *) log would-keep download-unreadable; echo "DRY-RUN: would KEEP: the download folder is unreadable (nothing closed)"; exit 0 ;;
85
+ esac
86
+ TABS="$("$D/chrome-unsaved.sh" "$P")"
87
+ printf '%s\n' "$TABS" | grep -q '^tab ' || TABS="none"
88
+ case "$TABS" in *done) ;; *) log would-keep unreadable-tabs; echo "DRY-RUN: would KEEP: the tabs could not be read (nothing closed)"; exit 0 ;; esac
89
+ if printf '%s\n' "$TABS" | grep -q ' yes$'; then
90
+ log would-keep unsaved-input; echo "DRY-RUN: would KEEP: a tab holds unsaved input; submit or clear it first (nothing closed)"; exit 0
91
+ fi
92
+ if printf '%s\n' "$TABS" | grep -q ' unreadable$'; then
93
+ log would-keep unreadable-tabs; echo "DRY-RUN: would KEEP: a tab could not be read (nothing closed)"; exit 0
94
+ fi
95
+ log would-close ""
96
+ echo "DRY-RUN: would close $1 (pid $PID); nothing closed in this release. Completion recorded."
97
+ exit 0
@@ -99,6 +99,8 @@ print(json.dumps({"ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
99
99
  "codeBytes": int(os.environ.get("NC_CALLER_CODE_BYTES") or len(code))}))' >> "$LOG"; } 2>/dev/null \
100
100
  || echo "warning: could not write the audit line to $LOG" >&2
101
101
  OUT="$(send "$WRAPPED")"
102
+ # Keep the browser ledger current: this browser is in use (renews its lease). Never fails the call.
103
+ "$(dirname "$0")/chrome-ledger.sh" touch "$P" 2>/dev/null
102
104
  case "$OUT" in
103
105
  "${NONCE}ok:"*) printf '%s\n' "${OUT#"${NONCE}ok:"}"; exit 0 ;;
104
106
  "${NONCE}err:"*) echo "ERR page-js-error: ${OUT#"${NONCE}err:"}"; exit 1 ;;
@@ -1,6 +1,10 @@
1
1
  #!/bin/bash
2
2
  # chrome-launch.sh — open a NORMAL (non-automated) Chrome for one browser profile.
3
- # Usage: chrome-launch.sh [--new] <profile-id|/absolute/user-data-dir> [url]
3
+ # Usage: chrome-launch.sh [--new] [--purpose <words>] [--ttl-minutes <n>] <profile-id|/absolute/user-data-dir> [url]
4
+ # Every launch is recorded in this agent's browser ledger (.instar/state/browser-instances/) with the
5
+ # session that opened it. --purpose says what it is for; --ttl-minutes declares how long its contents
6
+ # matter (a long scheduled browser action), after which the cleanup sweep may treat it as disposable.
7
+ # Close what you open with chrome-close.sh when the task ends.
4
8
  # A profile folder that does not exist is refused (it would open a signed-out browser that
5
9
  # looks like a lost login); pass --new to create one on purpose.
6
10
  # Launches through LaunchServices (`open -g -na`: in the background, never taking focus from the
@@ -14,8 +18,16 @@ if [ "$(uname -s)" != "Darwin" ]; then
14
18
  echo "ERR macos-only: the normal-Chrome method uses macOS Apple Events; it does not run on $(uname -s)" >&2
15
19
  exit 2
16
20
  fi
17
- NEWOK=0; [ "$1" = "--new" ] && { NEWOK=1; shift; }
18
- [ -z "$1" ] && { echo "usage: chrome-launch.sh [--new] <profile-id|/absolute/user-data-dir> [url]" >&2; exit 64; }
21
+ NEWOK=0; PURPOSE=""; TTL=""
22
+ while :; do
23
+ case "$1" in
24
+ --new) NEWOK=1; shift ;;
25
+ --purpose) [ -n "$2" ] || { echo "ERR bad-purpose: --purpose needs a value" >&2; exit 64; }; PURPOSE="$(printf '%s' "$2" | tr -cd 'A-Za-z0-9 ._:-' | cut -c1-80)"; shift 2 ;;
26
+ --ttl-minutes) printf '%s' "$2" | grep -Eq '^[1-9][0-9]{0,5}$' || { echo "ERR bad-ttl: --ttl-minutes takes a whole number of minutes" >&2; exit 64; }; TTL="$2"; shift 2 ;;
27
+ *) break ;;
28
+ esac
29
+ done
30
+ [ -z "$1" ] && { echo "usage: chrome-launch.sh [--new] [--purpose <words>] [--ttl-minutes <n>] <profile-id|/absolute/user-data-dir> [url]" >&2; exit 64; }
19
31
  # The url slot is passed to Chrome after --args, so anything that is not a URL would become a Chrome
20
32
  # flag. Only a URL with a scheme is accepted; a flag (--remote-debugging-port=..., -flag, /flag) is
21
33
  # refused before anything is launched. The same rule the Windows driver enforces with its allowlist.
@@ -87,5 +99,6 @@ os.replace(t, f) # atomic: Chrome never sees a half-written Preferences file
87
99
  # from a job or a tmux session must not take focus from whoever is using the machine (the operator
88
100
  # brings the window forward themselves when a page needs their own click).
89
101
  open -g -na "Google Chrome" --args --user-data-dir="$P" "$URL" || { echo "ERR launch-failed (is Google Chrome installed?)" >&2; exit 1; }
102
+ "$(dirname "$0")/chrome-ledger.sh" launch "$P" "$PURPOSE" "$TTL"
90
103
  [ "$NEW" = 1 ] && echo "note: new profile; Chrome ignores the Apple Events setting on its first run. Quit this Chrome once and run chrome-launch.sh again before chrome-js.sh." >&2
91
104
  exit 0
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # chrome-ledger.sh — keep this agent's browser-instance ledger (machine self-maintenance, Part B).
3
+ # Usage: chrome-ledger.sh launch|touch|renew|complete <canonical user-data-dir> [purpose] [ttl-minutes]
4
+ # Called by chrome-launch.sh, chrome-js.sh, chrome-renew.sh and chrome-close.sh with a profile
5
+ # directory they have already resolved inside this agent's profiles folder; it checks that again.
6
+ # One small JSON file per instance in .instar/state/browser-instances/, named by the first 16 hex
7
+ # of SHA-256 of the directory (the server's sweep uses the same name). It records who opened the
8
+ # browser and when it was last used; the sweep reads it to decide what it WOULD close. It closes
9
+ # nothing itself. A failure here never stops the caller: it warns on stderr and exits 0.
10
+ # Installed by instar into .instar/scripts/browser/ — overwritten on every update.
11
+ AGENT_HOME="$(cd "$(dirname "$0")/../../.." && pwd -P)"
12
+ ACTION="$1"; DIR="$2"
13
+ case "$ACTION" in launch|touch|renew|complete) ;; *) echo "usage: chrome-ledger.sh launch|touch|renew|complete <dir> [purpose] [ttl-minutes]" >&2; exit 64 ;; esac
14
+ case "$DIR" in "$AGENT_HOME/.instar/state/playwright-profiles/"?*) ;; *) echo "ERR ledger-dir-outside-profiles: $DIR" >&2; exit 64 ;; esac
15
+ LEDGER="$AGENT_HOME/.instar/state/browser-instances" ACTION="$ACTION" DIR="$DIR" PURPOSE="$3" TTL="$4" \
16
+ AGENT="${INSTAR_AGENT_ID:-}" SESSION="${INSTAR_SESSION_NAME:-}" TOPIC="${INSTAR_TELEGRAM_TOPIC:-}" /usr/bin/python3 -I -c '
17
+ import hashlib, json, os, time
18
+ e = os.environ
19
+ d, act = e["DIR"], e["ACTION"]
20
+ os.makedirs(e["LEDGER"], exist_ok=True)
21
+ f = os.path.join(e["LEDGER"], hashlib.sha256(d.encode()).hexdigest()[:16] + ".json")
22
+ now = time.time()
23
+ iso = lambda t: time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime(t))
24
+ cur = None
25
+ try:
26
+ with open(f) as h:
27
+ cur = json.load(h)
28
+ except Exception:
29
+ cur = None
30
+ if act == "launch":
31
+ declared = e["TTL"].isdigit() and int(e["TTL"]) > 0
32
+ ttl = int(e["TTL"]) if declared else 120
33
+ cur = {"userDataDir": d, "pid": None, "processStartTime": None, "agent": e["AGENT"] or None,
34
+ "sessionName": e["SESSION"] or None, "topicId": e["TOPIC"] or None, "purpose": e["PURPOSE"] or None,
35
+ "openedAt": iso(now), "lastUsedAt": iso(now), "ttlMinutes": ttl, "ttlDeclared": declared,
36
+ "leaseUntil": iso(now + ttl * 60), "completedAt": None}
37
+ elif cur is None:
38
+ raise SystemExit(0) # no entry: the sweep adopts a running browser it finds
39
+ elif act in ("touch", "renew"):
40
+ cur["lastUsedAt"] = iso(now)
41
+ cur["leaseUntil"] = iso(now + int(cur.get("ttlMinutes") or 120) * 60)
42
+ elif act == "complete":
43
+ cur["completedAt"] = iso(now)
44
+ t = f + ".tmp"
45
+ with open(t, "w") as h:
46
+ json.dump(cur, h, indent=2)
47
+ os.replace(t, f)
48
+ ' || echo "warning: could not update the browser ledger for $DIR" >&2
49
+ exit 0
@@ -0,0 +1,38 @@
1
+ #!/bin/bash
2
+ # chrome-renew.sh — renew the lease on one profile's browser for work the other helpers never see
3
+ # (a long download, a page working by itself). Usage: chrome-renew.sh <profile-id|/absolute/user-data-dir>
4
+ # chrome-js.sh and chrome-mainworld.sh renew it on every call already. macOS only.
5
+ # Installed by instar into .instar/scripts/browser/ — overwritten on every update.
6
+ if [ "$(uname -s)" != "Darwin" ]; then
7
+ echo "ERR macos-only: the normal-Chrome method uses macOS Apple Events; it does not run on $(uname -s)" >&2
8
+ exit 2
9
+ fi
10
+ [ -z "$1" ] && { echo "usage: chrome-renew.sh <profile-id|/absolute/user-data-dir>" >&2; exit 64; }
11
+ # Resolve the profile to a canonical user-data-dir STRICTLY INSIDE this agent's home (the same
12
+ # jail the Playwright profile registry applies). This guards against mistakes, not against a
13
+ # hostile caller with a shell: it keeps an ordinary call from reaching the operator's own Chrome. realpath resolves `..` and symlinks before the check, and the canonical
14
+ # path is what every later step uses (mkdir, Preferences, open, the process match).
15
+ AGENT_HOME="$(cd "$(dirname "$0")/../../.." && pwd -P)"
16
+ case "$AGENT_HOME" in *[[:space:]]*) echo "ERR agent-home-has-spaces: $AGENT_HOME (the exact process match needs a path without spaces; these tools cannot be used from this agent home)" >&2; exit 64 ;; esac
17
+ case "$1" in
18
+ /*) RAW="$1" ;;
19
+ */*|.*) echo "ERR bad-profile-id: use a profile id from GET /playwright-profiles or an absolute directory inside this agent's home" >&2; exit 64 ;;
20
+ *) printf '%s' "$1" | grep -Eq '^[a-z0-9-]{1,64}$' || { echo "ERR bad-profile-id: ids are lowercase letters, digits and dashes (the profile registry's rule)" >&2; exit 64; }
21
+ RAW="$AGENT_HOME/.instar/state/playwright-profiles/$1" ;;
22
+ esac
23
+ case "$RAW" in *[[:space:]]*) echo "ERR bad-profile-path: no spaces or tabs in a profile path (the process match could pick another profile)" >&2; exit 64 ;; esac
24
+ P="$(/usr/bin/python3 -I -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$RAW")" || { echo "ERR profile-resolve-failed: $RAW" >&2; exit 64; }
25
+ case "$P" in
26
+ "$AGENT_HOME"/?*) ;;
27
+ *) echo "ERR profile-outside-agent-home: $RAW (browser profiles must live under $AGENT_HOME)" >&2; exit 64 ;;
28
+ esac
29
+ # Inside the home, the agent's own folders are off limits except the profiles folder, and the
30
+ # operator's own Chrome folder is always refused (in case an agent home were ever ~).
31
+ case "$P/" in
32
+ "$HOME/Library/Application Support/Google/Chrome/"*) echo "ERR profile-reserved-folder: $P is the operator's own Chrome" >&2; exit 64 ;;
33
+ "$AGENT_HOME/.instar/state/playwright-profiles/"?*) ;;
34
+ "$AGENT_HOME/.instar/"*|"$AGENT_HOME/.claude/"*) echo "ERR profile-reserved-folder: $P is an agent config folder, not a browser profile" >&2; exit 64 ;;
35
+ esac
36
+ case "$P" in *[[:space:]]*) echo "ERR bad-profile-path: $P resolves to a path with spaces or tabs" >&2; exit 64 ;; esac
37
+ "$(dirname "$0")/chrome-ledger.sh" renew "$P"
38
+ echo "renewed"
@@ -0,0 +1,83 @@
1
+ #!/bin/bash
2
+ # chrome-unsaved.sh — read, for every tab of one profile's normal Chrome, whether it holds input a
3
+ # person or the agent typed and has not sent (machine self-maintenance, Part B).
4
+ # Usage: chrome-unsaved.sh <profile-id|/absolute/user-data-dir>
5
+ # Prints one line per tab, `tab <window>.<tab> yes|no|unreadable`, then `done`. Never prints a value.
6
+ # yes: an input or textarea whose value differs from its default, a checkbox, radio or select
7
+ # differing from its default, a chosen file, or a contenteditable element with any text (it has no
8
+ # default to compare with, so any text counts). unreadable: the tab did not answer in 5 seconds,
9
+ # the focus is inside a frame, or the page could not be read. Read-only: takes no focus, changes
10
+ # nothing on the page, runs in Chrome's isolated world. Used before any close; uncertainty keeps.
11
+ # macOS only (Apple Events). Installed by instar into .instar/scripts/browser/.
12
+ if [ "$(uname -s)" != "Darwin" ]; then
13
+ echo "ERR macos-only: the normal-Chrome method uses macOS Apple Events; it does not run on $(uname -s)" >&2
14
+ exit 2
15
+ fi
16
+ [ -z "$1" ] && { echo "usage: chrome-unsaved.sh <profile-id|/absolute/user-data-dir>" >&2; exit 64; }
17
+ # Resolve the profile to a canonical user-data-dir STRICTLY INSIDE this agent's home (the same
18
+ # jail the Playwright profile registry applies). This guards against mistakes, not against a
19
+ # hostile caller with a shell: it keeps an ordinary call from reaching the operator's own Chrome. realpath resolves `..` and symlinks before the check, and the canonical
20
+ # path is what every later step uses (mkdir, Preferences, open, the process match).
21
+ AGENT_HOME="$(cd "$(dirname "$0")/../../.." && pwd -P)"
22
+ case "$AGENT_HOME" in *[[:space:]]*) echo "ERR agent-home-has-spaces: $AGENT_HOME (the exact process match needs a path without spaces; these tools cannot be used from this agent home)" >&2; exit 64 ;; esac
23
+ case "$1" in
24
+ /*) RAW="$1" ;;
25
+ */*|.*) echo "ERR bad-profile-id: use a profile id from GET /playwright-profiles or an absolute directory inside this agent's home" >&2; exit 64 ;;
26
+ *) printf '%s' "$1" | grep -Eq '^[a-z0-9-]{1,64}$' || { echo "ERR bad-profile-id: ids are lowercase letters, digits and dashes (the profile registry's rule)" >&2; exit 64; }
27
+ RAW="$AGENT_HOME/.instar/state/playwright-profiles/$1" ;;
28
+ esac
29
+ case "$RAW" in *[[:space:]]*) echo "ERR bad-profile-path: no spaces or tabs in a profile path (the process match could pick another profile)" >&2; exit 64 ;; esac
30
+ P="$(/usr/bin/python3 -I -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$RAW")" || { echo "ERR profile-resolve-failed: $RAW" >&2; exit 64; }
31
+ case "$P" in
32
+ "$AGENT_HOME"/?*) ;;
33
+ *) echo "ERR profile-outside-agent-home: $RAW (browser profiles must live under $AGENT_HOME)" >&2; exit 64 ;;
34
+ esac
35
+ # Inside the home, the agent's own folders are off limits except the profiles folder, and the
36
+ # operator's own Chrome folder is always refused (in case an agent home were ever ~).
37
+ case "$P/" in
38
+ "$HOME/Library/Application Support/Google/Chrome/"*) echo "ERR profile-reserved-folder: $P is the operator's own Chrome" >&2; exit 64 ;;
39
+ "$AGENT_HOME/.instar/state/playwright-profiles/"?*) ;;
40
+ "$AGENT_HOME/.instar/"*|"$AGENT_HOME/.claude/"*) echo "ERR profile-reserved-folder: $P is an agent config folder, not a browser profile" >&2; exit 64 ;;
41
+ esac
42
+ case "$P" in *[[:space:]]*) echo "ERR bad-profile-path: $P resolves to a path with spaces or tabs" >&2; exit 64 ;; esac
43
+ PID=$(ps -axo pid=,args= | K="--user-data-dir=$P" awk '/Google Chrome\.app\/Contents\/MacOS\/Google Chrome / && index($0" ", ENVIRON["K"]" ") && !/--type=/ && !/--remote-debugging-|--enable-automation|--test-type/ { print $1; exit }')
44
+ [ -z "$PID" ] && { echo "NO_PID"; exit 1; }
45
+ NONCE="nc$(/usr/bin/python3 -I -c 'import secrets; print(secrets.token_hex(8))')" || { echo "ERR nonce-failed" >&2; exit 1; }
46
+ CHECK="(function(){var N='$NONCE';try{var a=document.activeElement;if(a&&(a.tagName==='IFRAME'||a.tagName==='FRAME'))return N+'unreadable';var y=false,els=document.querySelectorAll('input,textarea,select');for(var k=0;k<els.length&&!y;k++){var e=els[k],t=(e.type||'').toLowerCase();if(e.tagName==='SELECT'){for(var o=0;o<e.options.length;o++){if(e.options[o].selected!==e.options[o].defaultSelected)y=true}}else if(t==='checkbox'||t==='radio'){if(e.checked!==e.defaultChecked)y=true}else if(t==='file'){if(e.files&&e.files.length)y=true}else if(['hidden','submit','button','reset','image'].indexOf(t)<0){if(e.value!==e.defaultValue)y=true}}if(!y){var ce=document.querySelectorAll('[contenteditable]:not([contenteditable=false])');for(var c=0;c<ce.length;c++){if((ce[c].textContent||'').trim()){y=true;break}}}return N+(y?'yes':'no')}catch(x){return N+'unreadable'}})()"
47
+ JS="$CHECK" PID=$PID NONCE="$NONCE" osascript -l JavaScript <<'OSA'
48
+ ObjC.import('Foundation');
49
+ var env=$.NSProcessInfo.processInfo.environment;
50
+ var pid=parseInt(ObjC.unwrap(env.objectForKey('PID')));
51
+ var js=ObjC.unwrap(env.objectForKey('JS'));
52
+ var nonce=ObjC.unwrap(env.objectForKey('NONCE'));
53
+ var target=$.NSAppleEventDescriptor.descriptorWithProcessIdentifier(pid);
54
+ function fc(s){var b=s.split('').map(c=>c.charCodeAt(0));return (b[0]<<24)|(b[1]<<16)|(b[2]<<8)|b[3];}
55
+ function spec(cls,from,form,data){var r=$.NSAppleEventDescriptor.recordDescriptor.coerceToDescriptorType(fc('obj '));
56
+ r.setDescriptorForKeyword($.NSAppleEventDescriptor.descriptorWithTypeCode(fc(cls)),fc('want'));
57
+ r.setDescriptorForKeyword(from,fc('from'));
58
+ r.setDescriptorForKeyword($.NSAppleEventDescriptor.descriptorWithEnumCode(fc(form)),fc('form'));
59
+ r.setDescriptorForKeyword(data,fc('seld'));return r;}
60
+ var out=[], stop=false;
61
+ // errAENoSuchObject (-1728) ends a window's tabs, or the windows; any other failure is unreadable and stops the read.
62
+ for(var i=1;i<=50&&!stop;i++){
63
+ var w=spec('cwin',$.NSAppleEventDescriptor.nullDescriptor,'indx',$.NSAppleEventDescriptor.descriptorWithInt32(i));
64
+ for(var j=1;j<=200;j++){
65
+ var t=spec('ctab',w,'indx',$.NSAppleEventDescriptor.descriptorWithInt32(j));
66
+ var ev=$.NSAppleEventDescriptor.appleEventWithEventClassEventIDTargetDescriptorReturnIDTransactionID(fc('CrSu'),fc('ExJa'),target,-1,0);
67
+ ev.setParamDescriptorForKeyword(t,fc('----'));
68
+ ev.setParamDescriptorForKeyword($.NSAppleEventDescriptor.descriptorWithString(js),fc('JvSc'));
69
+ var err=Ref();
70
+ var res=ev.sendEventWithOptionsTimeoutError(0x3,5,err);
71
+ if(res.isNil()){
72
+ var code=(err[0]&&!err[0].isNil())?err[0].code:0;
73
+ if(code==-1728){ if(j==1) stop=true; break; }
74
+ out.push('tab '+i+'.'+j+' unreadable'); stop=true; break;
75
+ }
76
+ var d=res.paramDescriptorForKeyword(fc('----'));
77
+ var v=d.isNil()?'':ObjC.unwrap(d.stringValue);
78
+ out.push('tab '+i+'.'+j+' '+(v===nonce+'yes'?'yes':v===nonce+'no'?'no':'unreadable'));
79
+ }
80
+ }
81
+ out.push('done');
82
+ out.join('\n')
83
+ OSA
@@ -0,0 +1,68 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ New dev-gated server service (`docs/specs/ecosystem-idea-pipeline.md`). Each week it reviews a
9
+ watchlist of similar projects: trusted code fetches and reads, and AI services return closed-schema
10
+ judgments. Each idea is checked against our source and specs by two blind reads, reviewed against the
11
+ Instar 2.0 purpose, classed by what it would change, and ranked into bands. The default outcome is no
12
+ change. A framework reads outside text only after two must-fail file-read probes through the same call
13
+ path real reviews use; to pass them, `IntelligenceOptions.noLocalTools` and
14
+ `reviewerHardening.noTools` ask Codex and Claude for calls with no local tools. The default path of every
15
+ other caller is unchanged. `scripts/lint-ecosystem-adoption.mjs` joins the lint chain: it fails a default
16
+ change for a tagged spec unless `docs/proofs/<idea>.json` passes, and no spec is tagged yet.
17
+
18
+ ⚗️ Experimental, development agents only: copying runs wherever native app conversations are on (`mobilePairing.native.enabled`, dark on the fleet), even with `mobilePairing.native.media.enabled` off, so switching media off never strands a copy (as the spec requires); the media flag gates only the phone's media routes. Files sent in app conversations now reach every one of the agent's machines. Each machine copies every file that a message it holds refers to, over the existing signed machine-to-machine channel (a new `app-media-pull` verb), at most 1 MiB at a time, and keeps a copy only when its length and SHA-256 match the message. Copying runs in the background at a bounded pace (at most 8 slices every 5 seconds), pauses below the free-disk floor and, for at most an hour at a time, while the machine reports itself overloaded. A partial copy comes from one machine only (so a damaged copy is blamed on the machine that sent it), and a machine re-checks a file whose size or time changed before serving it, setting a damaged one aside. Brakes: a file no machine can provide is retried with a growing wait (30 seconds up to an hour) and given up after 7 days with one notice; a machine whose copies keep failing verification is skipped for 15 minutes, and for the rest of the day after the third time, with one notice. Each machine lists the files it holds every hour; `singleCopyBlobs` counts files only this machine holds, and a file left on one machine raises one notice after the other machines were asked to copy it and still have not. A download of a file still copying answers `202 fetching` when a copy can actually start, and otherwise `404` with the reason (no connection to the other machines, the disk floor, no reachable machine holding it, or given up); a message whose file has not reached the delivering machine within 60 seconds is delivered with `[file still copying: <name>]`, followed by one `[file now available: <path>]` line when it lands. `GET /health?scope=pool` shows every machine's `appMedia`.
19
+
20
+ - `GET /mobile/conversations` pages app conversations and Telegram topics on one cursor, so no conversation is cut from the list and left unreachable when there are many Telegram topics.
21
+ - `GET /mobile/conversations` and `GET /mobile/chat/app/:key/messages` accept `?scope=pool`: every machine's rows merged, a machine that did not answer listed in `pool.failed`, and a row only another machine holds marked "couldn't check here".
22
+ - With native app conversations on, a message sent from the app into a Telegram topic is no longer refused for a pairing invite, a desktop code or password-like text: the invite or code is removed and the rest delivered, password-like text is delivered with a `credentialShaped` signal, and the 📱 copy posted in the topic hides it.
23
+ - App history rows say how the signing phone was approved (`originApprovedVia`: `telegram-dm` or `dashboard-pin`), so the app can show "approved on dashboard".
24
+
25
+ - A later loss of an autonomous run's liveness check now gets its own 15-minute at-risk window after an earlier recovery returned the run to active. The old recovery attempt remains spent; it never refreshes the executor twice.
26
+ - A run that never recovers from the first loss still fails on the original deadline.
27
+
28
+ ## What to Tell Your User
29
+
30
+ If you develop your own project, I can check similar projects each week and show you ranked ideas, each
31
+ checked against your code. It only gives advice: it changes nothing and contacts nobody. It is off unless
32
+ you ask for it.
33
+
34
+ Nothing yet: this ships dark and needs the next iPhone app build. On a development agent with more than one machine, a file you send in the app is copied to my other machines, so any of them can open it.
35
+
36
+ If you use the Instar iPhone app with native conversations turned on: your conversation list now always shows every conversation, however many Telegram topics you have, and the app can show conversations from all your machines. A message you send from the app into a Telegram topic is no longer turned away because it contains an invite or something that looks like a password; the invite is removed, and the password-like text is sent as you wrote it (the app asks you first) but hidden in the copy posted to the Telegram topic.
37
+
38
+ A brief interruption after a previous recovered interruption will no longer stop an otherwise healthy autonomous run immediately. The run still pauses active work while any liveness check is red and fails if that interruption persists for 15 minutes.
39
+
40
+ ## Summary of New Capabilities
41
+
42
+ - `GET /ecosystem-ideas`, `GET /ecosystem-ideas/:id/evidence`, and PIN-gated decisions through `POST /ecosystem-ideas/plan` and `/commit`.
43
+ - The console's Work group gains an Ideas page.
44
+
45
+ - Mesh verb `app-media-pull` (slice, inventory, nudge), registered-peer read class.
46
+ - `GET /mobile/media/:sha256?ref=` answers `202 {state: "fetching", retryAfterMs}` while a file is copied here.
47
+ - `GET /app/media-path/:attachmentId` pulls a missing file for up to 30 seconds and says plainly why when it cannot.
48
+ - Authenticated `/health` → `appMedia.singleCopyBlobs` and `unrecoverableBlobs` are measured (null when they cannot be); `?scope=pool` adds `appMediaPool`.
49
+
50
+ - `?scope=pool` on `GET /mobile/conversations` and `GET /mobile/chat/app/:key/messages`.
51
+ - `originApprovedVia` on app history, sync and the device's Telegram history read.
52
+
53
+ - Autonomous run liveness tracks each later interruption's observation deadline correctly while keeping one recovery actuation per run.
54
+
55
+ ## Evidence
56
+
57
+ Live on this machine on 2026-11-01: without the no-tools form the capability probe refused Codex (it read
58
+ the planted file), and Claude's call stopped when it tried a tool. With the form, both frameworks passed
59
+ both probes. Tests: fixture runs on a real clone hold a hostile CLAUDE.md and a marker-writing script;
60
+ the planted idea is found, no marker is written, and the same run with file reads disabled finds nothing.
61
+
62
+ Unit tests for the serve side, verified pulls, backoff, the 7-day give-up, the peer breaker, the load and disk pauses, inventories and the single-copy alarm ladder; integration tests over a real signed MeshRpc client and dispatcher (202 then the verified bytes, a corrupted slice never kept, "still copying" then exactly one "now available" line, app-media-path pulling on demand, the pool health read); an end-to-end test of two production-booted servers copying a photo between them.
63
+
64
+ - Unit tests cover the list order and cursor, lossless paging across several sources, duplicate collapse, and hiding credentials in the mirror copy.
65
+ - Integration tests run the real routes: 250 Telegram topics plus 5 app conversations paged to the end with every row exactly once; pool reads with a live peer and an unreachable one; token removal through the Telegram route (stored as `redacted`, token absent from disk); the native-off refusals unchanged; the approval method for Telegram- and dashboard-approved phones.
66
+
67
+ - Unit tests cover successful and busy recovery, a later loss, healing, the 15-minute ceiling, and the same-millisecond boundary.
68
+ - Authenticated HTTP integration and production-boot E2E tests cover the route and full server path.
@@ -0,0 +1,24 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ - Honest lock reading: `GET /machine-maintenance/lock-state` reports whether a Mac is really locked. It says `locked` only when the session flag, the lock screen actually drawn and an immediate password requirement all agree. A screensaver a keypress dismisses reads `screensaver-or-display-sleep`; conflicting or unreadable evidence reads `unknown` with the reason. Every input is reported beside the verdict.
9
+ - Browser care, watch-only: `chrome-launch.sh` now records each normal Chrome an agent opens (which session, what for, how long it should live). A sweep every 15 minutes writes down which of this agent's own browsers it WOULD close and why it keeps the rest (unsaved form input, a download still running, someone at the keyboard, the opening session still alive, a sign-in repair). It never touches the operator's own Chrome and closes nothing in this release. New helpers: `chrome-close.sh` (dry-run: records that the task is done and reports what a close would do), `chrome-renew.sh`, `chrome-unsaved.sh`. Results ride `GET /machine-maintenance` as `browserCare`. Dev-gated, dark on the fleet.
10
+
11
+ ## What to Tell Your User
12
+
13
+ If you ask whether one of your Macs is locked, I can now check properly instead of trusting one flag that also says "locked" during a plain screensaver. I also keep a list of the browser windows I open, so I can tell you which ones I left open; for now I only note which I would close, and close none of them.
14
+
15
+ ## Summary of New Capabilities
16
+
17
+ - `GET /machine-maintenance/lock-state`: honest lock verdict with its inputs.
18
+ - `browserCare` on `GET /machine-maintenance`: the agent's open browsers and the sweep's would-close / keep verdicts.
19
+ - `chrome-close.sh`, `chrome-renew.sh`, `chrome-unsaved.sh` and `chrome-launch.sh --purpose/--ttl-minutes`.
20
+
21
+ ## Evidence
22
+
23
+ - Unit tests cover every row of the lock decision table on both sides, the browser sweep's ownership rule, each close reason and each keep reason, and the helper scripts with stubbed system tools; the page check runs in a real DOM. A source test proves the new code has no way to end a process.
24
+ - Integration and end-to-end tests serve both routes through the real server, including the 2026-11-01 case (flag set, no password, screensaver running), which now reads as a screensaver, not locked.
@@ -0,0 +1,98 @@
1
+ # Side-Effects Review — Machine self-maintenance, browser care and honest lock reading (watch-only)
2
+
3
+ **Version / slug:** `machine-self-maintenance-browser-care-lock-reading`
4
+ **Date:** `2026-11-02`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not run (--second-pass false, as instructed for this build)`
7
+
8
+ ## Summary of the change
9
+
10
+ Increment 2 of docs/specs/machine-self-maintenance.md: Part B (browser and window care, CMT-1201) in watch-only form, and Part A (honest lock reading, CMT-1204) as a read surface.
11
+
12
+ - Part A: `lockState.ts` is the spec's five-row decision table, pure. `MachineLockStateReader` (src/core) gathers its inputs on macOS from the packaged desktop helper's existing `probe` (run with the existing lock-input switch, which only adds read-only fields) and `sysadminctl -screenLock status`. `GET /machine-maintenance/lock-state` serves the verdict with every input beside it. Linux reads `not-applicable`, Windows and WSL read `unknown`.
13
+ - Part B: `BrowserCare.ts` holds the instance ledger and a 15-minute sweep that applies the spec's ownership rule, the three close grounds and the keep checks, and records what it WOULD close. It has no close path. New helpers `chrome-ledger.sh`, `chrome-close.sh` (dry-run: records the completion signal and reports), `chrome-renew.sh`, `chrome-unsaved.sh` (read-only per-tab check); `chrome-launch.sh` and `chrome-js.sh` now keep the ledger current. `GET /machine-maintenance` carries a `browserCare` block. An awareness section reaches fresh and updated agents, and the release note is `upgrades/next/machine-self-maintenance-browser-care-lock-reading.md`.
14
+
15
+ Tier 2 (spec-driven, the operator accepted the spec at the round cap). Size: 4 new source modules (about 650 lines), 4 new shell helpers, small edits to 2 helpers, wiring, 3 new test files and 3 extended ones. Effort: one design pass, no spec rounds in this increment.
16
+
17
+ Deliberate differences from the spec text, all in the safe direction:
18
+
19
+ 1. `chrome-close.sh` closes nothing in this release (the operator's instruction: anything that quits apps ships dry-run first). It records the completion signal, runs the same checks and logs `would-close` or `would-keep`.
20
+ 2. The launch cap does not refuse a launch while dry-run is on. The spec has the launcher refuse at the cap during dry-run; a dry-run that blocks launches is not a dry-run, so this build only reports `overAgentCap` and `overHostCap`. Enforcement arrives with closing.
21
+ 3. The host count is read from the process table (every agent's normal-Chrome profiles), not from marker files. Same number, no new host directory.
22
+ 4. A sign-in repair episode open on this machine keeps every browser (the spec excludes that episode's profile). Coarser, never wider.
23
+ 5. A `keepProfiles` allowlist is added (`machineMaintenance.browserCare.keepProfiles`): those profiles are never picked.
24
+ 6. The frontmost-window exclusion is replaced by a stronger rule: any console input in the last 5 minutes keeps every candidate.
25
+ 7. Part A's changes to `/desktop/diagnostics` and to the helper's capture admission are not in this increment; the lock reading is a separate read route. <!-- tracked: CMT-1204 -->
26
+
27
+ ## Decision-point inventory
28
+
29
+ - `lockState` verdict — add — deterministic table, reported only. Nothing in Instar reads it to admit, refuse or act; it answers "is that Mac really locked?" honestly.
30
+ - Browser sweep verdict (`would-close` / `keep` with a reason) — add — recorded evidence only. No code acts on it.
31
+ - `chrome-close.sh` verdict — add — printed and logged; it closes nothing.
32
+ - No admission, block, kill or message decision is added, modified or consulted.
33
+
34
+ ---
35
+
36
+ ## 1. Over-block
37
+
38
+ No block/allow surface. Nothing is refused or closed. The one behavioural change to an existing tool: `chrome-launch.sh` now also refuses a malformed `--ttl-minutes` or an empty `--purpose` (exit 64); calls without those flags behave as before.
39
+
40
+ ---
41
+
42
+ ## 2. Under-block
43
+
44
+ As evidence, known gaps, each visible:
45
+ - The per-tab check reads ordinary form fields, ticked boxes, selects, chosen files and contenteditable text. Text held only in a page's own memory or drawn on a canvas reads as no input. Any non-empty contenteditable reads as yes (it has no default to compare), so pages like editors are always kept: over-keeping, the safe side.
46
+ - Tabs are walked by window index; if the window order changed mid-read a tab could be read twice and another missed. The check runs only after 5 minutes with no console input, which makes a reorder unlikely, and in this release nothing closes on its answer.
47
+ - A Chrome with no windows reads as unreadable and is kept.
48
+ - Only the real Chrome app's main process counts (`Google Chrome.app/Contents/MacOS/Google Chrome`). Chrome for Testing, Chromium and any browser started with automation flags (`--remote-debugging-port/pipe`, `--enable-automation`, `--test-type`) are never counted, even inside a profiles folder, so the Playwright MCP browser is never adopted. The same rule applies to the PID lookup in `chrome-unsaved.sh` and `chrome-close.sh`. Added after the independent review of PR 2582.
49
+ - The sweep ignores `relogin-chrome-profile-*` directories and anything outside this agent's profiles folder, so those are never counted as owned.
50
+ - Part A: a lock shield drawn while the flag is set and no password is required is conflicting evidence and reads `unknown` (row 5), not `screensaver-or-display-sleep`; row 3 requires no shield. Added after the independent review.
51
+ - Part A: a stale or missing lock sample, an old helper without lock inputs, or an unreadable policy all read `unknown`. That is honest but means `unknown` may be common until the helper is present everywhere; the live matrix (L1) is still to run.
52
+
53
+ ---
54
+
55
+ ## 3. Level-of-abstraction fit
56
+
57
+ The decision logic lives in `monitoring/machineMaintenance/` beside the drain rehearsal and under the same source rule (no way to signal a process, OS commands only through the fixed read-only table). The two reads that must run a helper (`chrome-unsaved.sh`) or read a profile's folders live in `src/core/BrowserCareProbes.ts`, injected as dependencies. The lock reader reuses the existing desktop helper and its existing `lockInputs` rather than adding a native verb, and keeps the spec's table separate from the desktop-locked-screen classifier, which answers a different question (whether remote control may proceed).
58
+
59
+ ---
60
+
61
+ ## 4. Signal vs authority compliance
62
+
63
+ Signal only. `tests/unit/machine-maintenance-watch-only.test.ts` now covers `BrowserCare.ts` and `lockState.ts` too, and separately the two reads kept in src/core (`BrowserCareProbes.ts`, `MachineLockStateReader.ts`): no restart words, no process signal, no file writes, only their named commands with fixed arguments, and an import allowlist. In the maintenance folder: no restart words, no way to signal a process, exec only through the one allowlisted runner (which gained one read: `ps -axo pid=,lstart=,args=`). `chrome-close.sh` is checked by `tests/unit/browser-care-scripts.test.ts` to hold no kill, quit or Apple Event. `chrome-unsaved.sh`'s page code is checked to assign nothing and to send one event type only. Reference: docs/signal-vs-authority.md.
64
+
65
+ ---
66
+
67
+ ## 5. Interactions
68
+
69
+ - `chrome-launch.sh` and `chrome-js.sh` call `chrome-ledger.sh` after their own work; a ledger failure warns on stderr and never changes their exit code or output.
70
+ - The sweep's per-tab check sends a read-only Apple Event to an agent-owned Chrome. It runs only for close candidates, at most 5 per pass, only after 5 minutes without console input. If macOS has not yet allowed the server's process tree to control Chrome, that first event can show macOS's own permission prompt on the machine. Sessions already drive these same Chromes through the same tmux tree, so the grant normally exists; the bound keeps any prompt rare.
71
+ - The lock reader runs the helper's `probe` with the lock-input switch. On that verb the switch only adds fields; it starts no indicator, takes no lock and grants nothing. Cached 5 s, one read at a time.
72
+ - The drain rehearsal, `DirectDesktopAccess` and `DesktopLockedController` are not touched.
73
+
74
+ ---
75
+
76
+ ## 6. External surfaces
77
+
78
+ New route `GET /machine-maintenance/lock-state` (Bearer, 503 when dark). New field `browserCare` on `GET /machine-maintenance`. New state `.instar/state/browser-instances/` (one file per open owned browser, removed when it exits) and `logs/browser-care.jsonl` (rotated at 2 MiB), both registered in the state coherence registry. Four new helper scripts in `.instar/scripts/browser/`, installed by the existing always-overwrite installer. One CLAUDE.md section, added to existing agents by `migrateClaudeMd` with a content-sniff guard. Everything runs only where `machineMaintenance` is on (dev-gated; dark on the fleet); the helper scripts install everywhere, and the ledger they write is inert where the sweep is dark.
79
+
80
+ ---
81
+
82
+ ## 7. Multi-machine posture
83
+
84
+ Machine-local by design (hardware-bound-resource): the ledger and verdicts describe live processes on one machine, and the lock reading is that machine's own console. `?scope=pool` for these fields is part of the later pool increment. <!-- tracked: CMT-1199 -->
85
+
86
+ ---
87
+
88
+ ## 8. Rollback cost
89
+
90
+ `machineMaintenance.browserCare.enabled: false` stops the sweep (read live). `machineMaintenance.enabled: false` removes both the sweep and the lock route at the next restart. Reverting the release leaves inert ledger files and the extra helper scripts; nothing reads them. No data migration, no state repair.
91
+
92
+ ## Second-pass review
93
+
94
+ **Reviewer:** independent subagent, second pass, 2026-11-02
95
+
96
+ **Verdict: concur**, after a confirming pass on 58b722a80. Findings fixed: automation browsers (Chrome for Testing, Playwright) excluded from the sweep and scripts; the cannot-act test covers BrowserCareProbes.ts and MachineLockStateReader.ts; a lock shield with no password reads unknown. Known low items: the scripts exclude --remote-debugging- as a prefix while the parser lists port and pipe; chrome-close.sh records finished before its checks; a missing sign-in repair system reads as no repair open. The two skipIf(!HAS_PY) skips are justified: /usr/bin/python3 is what the scripts themselves require.
97
+
98
+ **Suite ratchet, 2026-11-02:** no-silent-fallbacks flagged seven new catches. Each turns a failed read into an explicit unknown or unreadable that keeps the browser or makes the lock verdict unknown; they now carry the @silent-fallback-ok annotation. Comments only, no logic change.