instar 0.7.49 → 0.7.50

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.
@@ -646,50 +646,19 @@ If Telegram was set up in Phase 3, install the relay script that lets Claude ses
646
646
  mkdir -p .claude/scripts
647
647
  ```
648
648
 
649
- Write `.claude/scripts/telegram-reply.sh`:
649
+ **IMPORTANT: Do NOT write a custom telegram-reply.sh.** Instead, copy the canonical version from the instar package:
650
650
 
651
651
  ```bash
652
- #!/bin/bash
653
- # telegram-reply.sh — Send a message back to a Telegram topic via instar server.
654
- #
655
- # Usage:
656
- # .claude/scripts/telegram-reply.sh TOPIC_ID "message text"
657
- # echo "message text" | .claude/scripts/telegram-reply.sh TOPIC_ID
658
- # cat <<'EOF' | .claude/scripts/telegram-reply.sh TOPIC_ID
659
- # Multi-line message here
660
- # EOF
661
-
662
- TOPIC_ID=$1
663
- shift
664
-
665
- if [ -z "$TOPIC_ID" ]; then
666
- echo "Usage: telegram-reply.sh TOPIC_ID [message]" >&2
667
- exit 1
668
- fi
669
-
670
- PORT=<PORT>
671
-
672
- # Get message from args or stdin
673
- if [ $# -gt 0 ]; then
674
- MESSAGE="$*"
675
- else
676
- MESSAGE=$(cat)
677
- fi
678
-
679
- if [ -z "$MESSAGE" ]; then
680
- echo "No message provided" >&2
681
- exit 1
682
- fi
683
-
684
- # Send via instar server API
685
- curl -s -X POST "http://localhost:${PORT}/telegram/topic/${TOPIC_ID}/send" \
686
- -H 'Content-Type: application/json' \
687
- -d "$(jq -n --arg text "$MESSAGE" '{text: $text}')" > /dev/null 2>&1
688
-
689
- echo "Sent $(echo "$MESSAGE" | wc -c | tr -d ' ') chars to topic $TOPIC_ID"
652
+ cp "$(dirname "$(which instar 2>/dev/null || echo "$(npm root -g)/instar")")/templates/scripts/telegram-reply.sh" .claude/scripts/telegram-reply.sh 2>/dev/null
690
653
  ```
691
654
 
692
- Replace `<PORT>` with the actual server port. Then make it executable:
655
+ If the copy fails (e.g., npx install), write the script using the template at `node_modules/instar/dist/templates/scripts/telegram-reply.sh` as the source. The key details:
656
+ - **Endpoint**: `POST http://localhost:PORT/telegram/reply/TOPIC_ID` (NOT `/telegram/topic/TOPIC_ID/send`)
657
+ - **Auth**: Must read authToken from `.instar/config.json` and include `Authorization: Bearer TOKEN` header
658
+ - **JSON escaping**: Use python3 for proper JSON escaping, not jq (which may not be installed)
659
+ - **Error reporting**: Do NOT pipe curl output to `/dev/null` — check the HTTP status code and report failures
660
+
661
+ Then make it executable:
693
662
 
694
663
  ```bash
695
664
  chmod +x .claude/scripts/telegram-reply.sh
@@ -0,0 +1,11 @@
1
+ > Why do I have a folder named ".vercel" in my project?
2
+ The ".vercel" folder is created when you link a directory to a Vercel project.
3
+
4
+ > What does the "project.json" file contain?
5
+ The "project.json" file contains:
6
+ - The ID of the Vercel project that you linked ("projectId")
7
+ - The ID of the user or team your Vercel project is owned by ("orgId")
8
+
9
+ > Should I commit the ".vercel" folder?
10
+ No, you should not share the ".vercel" folder with anyone.
11
+ Upon creation, it will be automatically added to your ".gitignore" file.
@@ -0,0 +1 @@
1
+ {"projectId":"prj_evM5LcItYL3IAmw8zNvEPGrHeaya","orgId":"team_dHctwIDcV3X9ydapQlCPHFGI","projectName":"claude-agent-kit"}
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { execFileSync } from 'node:child_process';
11
11
  import fs from 'node:fs';
12
+ import os from 'node:os';
12
13
  import path from 'node:path';
13
14
  import pc from 'picocolors';
14
15
  import { loadConfig, ensureStateDir, detectTmuxPath } from '../core/Config.js';
@@ -35,6 +36,24 @@ import { QuotaTracker } from '../monitoring/QuotaTracker.js';
35
36
  import { AccountSwitcher } from '../monitoring/AccountSwitcher.js';
36
37
  import { QuotaNotifier } from '../monitoring/QuotaNotifier.js';
37
38
  import { classifySessionDeath } from '../monitoring/QuotaExhaustionDetector.js';
39
+ import { installAutoStart } from './setup.js';
40
+ /**
41
+ * Check if autostart is installed for this project.
42
+ * Extracted from the CLI `autostart status` handler for programmatic use.
43
+ */
44
+ function isAutostartInstalled(projectName) {
45
+ if (process.platform === 'darwin') {
46
+ const label = `ai.instar.${projectName}`;
47
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
48
+ return fs.existsSync(plistPath);
49
+ }
50
+ else if (process.platform === 'linux') {
51
+ const serviceName = `instar-${projectName}.service`;
52
+ const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', serviceName);
53
+ return fs.existsSync(servicePath);
54
+ }
55
+ return false;
56
+ }
38
57
  /**
39
58
  * Respawn a session for a topic, including thread history in the bootstrap.
40
59
  * This prevents "thread drift" where respawned sessions lose context.
@@ -710,6 +729,77 @@ export async function startServer(options) {
710
729
  ...(config.evolution || {}),
711
730
  });
712
731
  console.log(pc.green(' Evolution system enabled'));
732
+ // Start MemoryPressureMonitor (platform-aware memory tracking)
733
+ const { MemoryPressureMonitor } = await import('../monitoring/MemoryPressureMonitor.js');
734
+ const memoryMonitor = new MemoryPressureMonitor({});
735
+ memoryMonitor.on('stateChange', ({ from, to, state: memState }) => {
736
+ // Gate scheduler spawning on memory pressure
737
+ if (scheduler && (to === 'elevated' || to === 'critical')) {
738
+ console.log(`[MemoryPressure] ${from} -> ${to} — scheduler should respect canSpawnSession()`);
739
+ }
740
+ // Alert via Telegram attention topic
741
+ if (telegram && to !== 'normal') {
742
+ const attentionTopicId = state.get('agent-attention-topic');
743
+ if (attentionTopicId) {
744
+ telegram.sendToTopic(attentionTopicId, `Memory ${to}: ${memState.pressurePercent.toFixed(1)}% used, ${memState.freeGB.toFixed(1)}GB free (trend: ${memState.trend})`).catch(() => { });
745
+ }
746
+ }
747
+ });
748
+ memoryMonitor.start();
749
+ // Wire memory gate into scheduler
750
+ if (scheduler) {
751
+ const originalCanRun = scheduler.canRunJob;
752
+ scheduler.canRunJob = (priority) => {
753
+ // Check memory first
754
+ const memCheck = memoryMonitor.canSpawnSession();
755
+ if (!memCheck.allowed) {
756
+ return false;
757
+ }
758
+ // Then check original gate (quota, etc.)
759
+ return originalCanRun(priority);
760
+ };
761
+ }
762
+ // Start CaffeinateManager (prevents macOS system sleep)
763
+ const { CaffeinateManager } = await import('../core/CaffeinateManager.js');
764
+ const caffeinateManager = new CaffeinateManager({ stateDir: config.stateDir });
765
+ caffeinateManager.start();
766
+ // Start SleepWakeDetector (re-validate sessions on wake)
767
+ const { SleepWakeDetector } = await import('../core/SleepWakeDetector.js');
768
+ const sleepWakeDetector = new SleepWakeDetector();
769
+ sleepWakeDetector.on('wake', async (event) => {
770
+ console.log(`[SleepWake] Wake detected after ~${event.sleepDurationSeconds}s sleep`);
771
+ // Re-validate tmux sessions
772
+ try {
773
+ const tmuxPath = detectTmuxPath();
774
+ if (tmuxPath) {
775
+ const { execFileSync } = await import('child_process');
776
+ const result = execFileSync(tmuxPath, ['list-sessions'], { encoding: 'utf-8', timeout: 5000 }).trim();
777
+ console.log(`[SleepWake] tmux sessions after wake: ${result.split('\n').length}`);
778
+ }
779
+ }
780
+ catch {
781
+ console.warn('[SleepWake] tmux check failed after wake');
782
+ }
783
+ // Restart tunnel if configured
784
+ if (tunnel) {
785
+ try {
786
+ await tunnel.stop();
787
+ const tunnelUrl = await tunnel.start();
788
+ console.log(`[SleepWake] Tunnel restarted: ${tunnelUrl}`);
789
+ }
790
+ catch (err) {
791
+ console.error(`[SleepWake] Tunnel restart failed:`, err);
792
+ }
793
+ }
794
+ // Notify via Telegram attention topic
795
+ if (telegram) {
796
+ const attentionTopicId = state.get('agent-attention-topic');
797
+ if (attentionTopicId) {
798
+ telegram.sendToTopic(attentionTopicId, `Wake detected after ~${event.sleepDurationSeconds}s sleep. Sessions re-validated.`).catch(() => { });
799
+ }
800
+ }
801
+ });
802
+ sleepWakeDetector.start();
713
803
  const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, publisher, viewer, tunnel, evolution });
714
804
  await server.start();
715
805
  // Start tunnel AFTER server is listening
@@ -723,9 +813,33 @@ export async function startServer(options) {
723
813
  console.log(pc.yellow(` Server running locally without tunnel. Fix tunnel config and restart.`));
724
814
  }
725
815
  }
816
+ // Self-healing: ensure autostart is installed so the server always restarts
817
+ // This is a non-negotiable requirement — the user must always be able to reach their agent remotely.
818
+ // If autostart isn't installed, install it silently. The agent should never require human intervention
819
+ // to ensure its own resilience.
820
+ try {
821
+ const hasTelegram = !!telegram;
822
+ const autostartInstalled = isAutostartInstalled(config.projectName);
823
+ if (!autostartInstalled) {
824
+ const installed = installAutoStart(config.projectName, config.projectDir, hasTelegram);
825
+ if (installed) {
826
+ console.log(pc.green(` Auto-start self-healed: installed ${process.platform === 'darwin' ? 'LaunchAgent' : 'systemd service'}`));
827
+ }
828
+ else {
829
+ console.log(pc.yellow(` Auto-start not available on ${process.platform}`));
830
+ }
831
+ }
832
+ }
833
+ catch (err) {
834
+ // Non-critical — don't crash the server over autostart
835
+ console.error(` Auto-start check failed: ${err instanceof Error ? err.message : err}`);
836
+ }
726
837
  // Graceful shutdown
727
838
  const shutdown = async () => {
728
839
  console.log('\nShutting down...');
840
+ memoryMonitor.stop();
841
+ caffeinateManager.stop();
842
+ sleepWakeDetector.stop();
729
843
  autoUpdater.stop();
730
844
  autoDispatcher?.stop();
731
845
  if (tunnel)
@@ -122,7 +122,8 @@ export class AutoUpdater {
122
122
  if (!this.config.autoApply) {
123
123
  // Just notify — don't apply
124
124
  await this.notify(`Update available: v${info.currentVersion} → v${info.latestVersion}\n\n` +
125
- (info.changeSummary ? `Changes: ${info.changeSummary}\n\n` : '') +
125
+ (info.changeSummary ? `What changed:\n${info.changeSummary}\n\n` : '') +
126
+ `Details: ${info.changelogUrl || 'https://github.com/SageMindAI/instar/releases'}\n\n` +
126
127
  `Auto-apply is disabled. Apply manually:\n` +
127
128
  `curl -X POST http://localhost:${this.getPort()}/updates/apply`);
128
129
  return;
@@ -147,13 +148,19 @@ export class AutoUpdater {
147
148
  console.log(`[AutoUpdater] Updated: v${result.previousVersion} → v${result.newVersion}`);
148
149
  // Step 5: Notify via Telegram
149
150
  const restartNote = result.restartNeeded && this.config.autoRestart
150
- ? 'Server is restarting now...'
151
+ ? '\nServer is restarting now...'
151
152
  : result.restartNeeded
152
- ? 'A server restart is needed to use the new version.'
153
+ ? '\nA server restart is needed to use the new version.'
153
154
  : '';
155
+ const changeSummary = info.changeSummary
156
+ ? `What changed:\n${info.changeSummary}\n`
157
+ : '';
158
+ const detailsUrl = info.changelogUrl || 'https://github.com/SageMindAI/instar/releases';
154
159
  await this.notify(`Updated: v${result.previousVersion} → v${result.newVersion}\n\n` +
155
- (info.changeSummary ? `What changed:\n${info.changeSummary}\n\n` : '') +
156
- restartNote);
160
+ changeSummary +
161
+ `Details: ${detailsUrl}\n` +
162
+ restartNote +
163
+ `\n\nTo disable auto-updates, set "autoApply": false in .instar/config.json under "updates".`);
157
164
  // Step 6: Self-restart if needed and configured
158
165
  if (result.restartNeeded && this.config.autoRestart) {
159
166
  // Brief delay to let the Telegram notification send
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CaffeinateManager - Prevent macOS system sleep for Instar server lifetime.
3
+ *
4
+ * Maintains a `caffeinate -s` child process that prevents system sleep.
5
+ * A watchdog verifies it's alive every 30s and restarts if dead.
6
+ * PID is written to <stateDir>/caffeinate.pid for crash recovery.
7
+ *
8
+ * Only activates on macOS (process.platform === 'darwin').
9
+ * Uses EventEmitter pattern consistent with Instar conventions.
10
+ */
11
+ import { EventEmitter } from 'node:events';
12
+ export interface CaffeinateManagerConfig {
13
+ /** State directory for PID file storage */
14
+ stateDir: string;
15
+ }
16
+ export interface CaffeinateStatus {
17
+ running: boolean;
18
+ pid: number | null;
19
+ startedAt: string | null;
20
+ restartCount: number;
21
+ lastWatchdogCheck: string;
22
+ }
23
+ export declare class CaffeinateManager extends EventEmitter {
24
+ private process;
25
+ private watchdogInterval;
26
+ private pid;
27
+ private startedAt;
28
+ private restartCount;
29
+ private lastWatchdogCheck;
30
+ private stopping;
31
+ private pidFile;
32
+ constructor(config: CaffeinateManagerConfig);
33
+ /**
34
+ * Start caffeinate and the watchdog.
35
+ * Only activates on macOS.
36
+ */
37
+ start(): void;
38
+ /**
39
+ * Stop caffeinate and the watchdog cleanly.
40
+ */
41
+ stop(): void;
42
+ getStatus(): CaffeinateStatus;
43
+ private spawnCaffeinate;
44
+ private killCaffeinate;
45
+ private watchdog;
46
+ private cleanupStale;
47
+ private writePidFile;
48
+ private removePidFile;
49
+ }
50
+ //# sourceMappingURL=CaffeinateManager.d.ts.map
@@ -0,0 +1,180 @@
1
+ /**
2
+ * CaffeinateManager - Prevent macOS system sleep for Instar server lifetime.
3
+ *
4
+ * Maintains a `caffeinate -s` child process that prevents system sleep.
5
+ * A watchdog verifies it's alive every 30s and restarts if dead.
6
+ * PID is written to <stateDir>/caffeinate.pid for crash recovery.
7
+ *
8
+ * Only activates on macOS (process.platform === 'darwin').
9
+ * Uses EventEmitter pattern consistent with Instar conventions.
10
+ */
11
+ import { EventEmitter } from 'node:events';
12
+ import { spawn, execSync } from 'child_process';
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ const WATCHDOG_INTERVAL_MS = 30_000; // 30 seconds
16
+ export class CaffeinateManager extends EventEmitter {
17
+ process = null;
18
+ watchdogInterval = null;
19
+ pid = null;
20
+ startedAt = null;
21
+ restartCount = 0;
22
+ lastWatchdogCheck = new Date().toISOString();
23
+ stopping = false;
24
+ pidFile;
25
+ constructor(config) {
26
+ super();
27
+ this.pidFile = path.join(config.stateDir, 'caffeinate.pid');
28
+ }
29
+ /**
30
+ * Start caffeinate and the watchdog.
31
+ * Only activates on macOS.
32
+ */
33
+ start() {
34
+ if (this.watchdogInterval)
35
+ return;
36
+ if (process.platform !== 'darwin') {
37
+ console.log('[CaffeinateManager] Not macOS — skipping sleep prevention');
38
+ return;
39
+ }
40
+ this.stopping = false;
41
+ this.cleanupStale();
42
+ this.spawnCaffeinate();
43
+ this.watchdogInterval = setInterval(() => this.watchdog(), WATCHDOG_INTERVAL_MS);
44
+ this.watchdogInterval.unref(); // Don't prevent process exit
45
+ console.log(`[CaffeinateManager] Started (watchdog: ${WATCHDOG_INTERVAL_MS / 1000}s)`);
46
+ }
47
+ /**
48
+ * Stop caffeinate and the watchdog cleanly.
49
+ */
50
+ stop() {
51
+ this.stopping = true;
52
+ if (this.watchdogInterval) {
53
+ clearInterval(this.watchdogInterval);
54
+ this.watchdogInterval = null;
55
+ }
56
+ this.killCaffeinate();
57
+ this.removePidFile();
58
+ console.log('[CaffeinateManager] Stopped');
59
+ }
60
+ getStatus() {
61
+ return {
62
+ running: this.process !== null && this.pid !== null,
63
+ pid: this.pid,
64
+ startedAt: this.startedAt,
65
+ restartCount: this.restartCount,
66
+ lastWatchdogCheck: this.lastWatchdogCheck,
67
+ };
68
+ }
69
+ spawnCaffeinate() {
70
+ try {
71
+ const proc = spawn('caffeinate', ['-s'], {
72
+ detached: true,
73
+ stdio: 'ignore',
74
+ });
75
+ proc.unref();
76
+ this.process = proc;
77
+ this.pid = proc.pid ?? null;
78
+ this.startedAt = new Date().toISOString();
79
+ this.writePidFile();
80
+ proc.on('exit', (code, signal) => {
81
+ if (!this.stopping) {
82
+ console.warn(`[CaffeinateManager] caffeinate exited (code: ${code}, signal: ${signal})`);
83
+ this.emit('died', { code, signal });
84
+ }
85
+ this.process = null;
86
+ this.pid = null;
87
+ });
88
+ proc.on('error', (err) => {
89
+ console.error('[CaffeinateManager] caffeinate spawn error:', err.message);
90
+ this.process = null;
91
+ this.pid = null;
92
+ });
93
+ console.log(`[CaffeinateManager] caffeinate spawned (PID: ${this.pid})`);
94
+ this.emit('started', { pid: this.pid });
95
+ }
96
+ catch (err) {
97
+ console.error('[CaffeinateManager] Failed to spawn caffeinate:', err);
98
+ }
99
+ }
100
+ killCaffeinate() {
101
+ if (this.pid) {
102
+ try {
103
+ process.kill(this.pid, 'SIGTERM');
104
+ }
105
+ catch {
106
+ // Already dead
107
+ }
108
+ }
109
+ this.process = null;
110
+ this.pid = null;
111
+ }
112
+ watchdog() {
113
+ this.lastWatchdogCheck = new Date().toISOString();
114
+ if (this.stopping)
115
+ return;
116
+ if (this.pid) {
117
+ try {
118
+ process.kill(this.pid, 0);
119
+ return; // Still alive
120
+ }
121
+ catch {
122
+ console.warn(`[CaffeinateManager] caffeinate PID ${this.pid} is dead`);
123
+ this.process = null;
124
+ this.pid = null;
125
+ }
126
+ }
127
+ this.restartCount++;
128
+ console.log(`[CaffeinateManager] Restarting caffeinate (restart #${this.restartCount})`);
129
+ this.spawnCaffeinate();
130
+ this.emit('restarted', { restartCount: this.restartCount });
131
+ }
132
+ cleanupStale() {
133
+ try {
134
+ if (fs.existsSync(this.pidFile)) {
135
+ const stalePid = parseInt(fs.readFileSync(this.pidFile, 'utf-8').trim(), 10);
136
+ if (!isNaN(stalePid) && stalePid > 0) {
137
+ try {
138
+ const cmdline = execSync(`ps -p ${stalePid} -o comm= 2>/dev/null`, {
139
+ encoding: 'utf-8',
140
+ timeout: 3000,
141
+ }).trim();
142
+ if (cmdline.includes('caffeinate')) {
143
+ process.kill(stalePid, 'SIGTERM');
144
+ console.log(`[CaffeinateManager] Killed stale caffeinate (PID: ${stalePid})`);
145
+ }
146
+ }
147
+ catch {
148
+ // Process doesn't exist
149
+ }
150
+ }
151
+ this.removePidFile();
152
+ }
153
+ }
154
+ catch {
155
+ // PID file doesn't exist or can't be read
156
+ }
157
+ }
158
+ writePidFile() {
159
+ if (!this.pid)
160
+ return;
161
+ try {
162
+ fs.mkdirSync(path.dirname(this.pidFile), { recursive: true });
163
+ fs.writeFileSync(this.pidFile, String(this.pid));
164
+ }
165
+ catch (err) {
166
+ console.error('[CaffeinateManager] Failed to write PID file:', err);
167
+ }
168
+ }
169
+ removePidFile() {
170
+ try {
171
+ if (fs.existsSync(this.pidFile)) {
172
+ fs.unlinkSync(this.pidFile);
173
+ }
174
+ }
175
+ catch {
176
+ // Not critical
177
+ }
178
+ }
179
+ }
180
+ //# sourceMappingURL=CaffeinateManager.js.map
@@ -60,7 +60,8 @@ export declare class UpdateChecker {
60
60
  updatedAt: string;
61
61
  } | null;
62
62
  /**
63
- * Fetch human-readable changelog from GitHub releases.
63
+ * Fetch human-readable changelog from GitHub releases, falling back to
64
+ * recent commit messages if no release exists for this version.
64
65
  */
65
66
  fetchChangelog(version: string): Promise<string | undefined>;
66
67
  /**
@@ -226,9 +226,11 @@ export class UpdateChecker {
226
226
  }
227
227
  }
228
228
  /**
229
- * Fetch human-readable changelog from GitHub releases.
229
+ * Fetch human-readable changelog from GitHub releases, falling back to
230
+ * recent commit messages if no release exists for this version.
230
231
  */
231
232
  async fetchChangelog(version) {
233
+ // Try GitHub release first
232
234
  try {
233
235
  const tag = version.startsWith('v') ? version : `v${version}`;
234
236
  const response = await fetch(`${GITHUB_RELEASES_URL}/tags/${tag}`, {
@@ -238,16 +240,41 @@ export class UpdateChecker {
238
240
  },
239
241
  signal: AbortSignal.timeout(10000),
240
242
  });
241
- if (!response.ok)
242
- return undefined;
243
- const release = await response.json();
244
- if (release.body) {
245
- // Truncate to first 500 chars for concise summary
246
- const summary = release.body.slice(0, 500);
247
- return summary.length < release.body.length ? summary + '...' : summary;
243
+ if (response.ok) {
244
+ const release = await response.json();
245
+ if (release.body) {
246
+ const summary = release.body.slice(0, 500);
247
+ return summary.length < release.body.length ? summary + '...' : summary;
248
+ }
249
+ if (release.name)
250
+ return release.name;
251
+ }
252
+ }
253
+ catch {
254
+ // Non-critical — try commit fallback
255
+ }
256
+ // Fallback: fetch recent commits from GitHub
257
+ try {
258
+ const response = await fetch('https://api.github.com/repos/SageMindAI/instar/commits?per_page=5', {
259
+ headers: {
260
+ 'Accept': 'application/vnd.github.v3+json',
261
+ 'User-Agent': 'instar-update-checker',
262
+ },
263
+ signal: AbortSignal.timeout(10000),
264
+ });
265
+ if (response.ok) {
266
+ const commits = await response.json();
267
+ if (commits.length > 0) {
268
+ const lines = commits
269
+ .map(c => {
270
+ // Take first line of commit message only
271
+ const firstLine = c.commit.message.split('\n')[0];
272
+ return `• ${firstLine}`;
273
+ })
274
+ .join('\n');
275
+ return `Recent changes:\n${lines}`;
276
+ }
248
277
  }
249
- if (release.name)
250
- return release.name;
251
278
  }
252
279
  catch {
253
280
  // Non-critical
@@ -25,6 +25,8 @@ export declare class ServerSupervisor extends EventEmitter {
25
25
  private restartBackoffMs;
26
26
  private isRunning;
27
27
  private lastHealthy;
28
+ private startupGraceMs;
29
+ private spawnedAt;
28
30
  constructor(options: {
29
31
  projectDir: string;
30
32
  projectName: string;
@@ -22,6 +22,8 @@ export class ServerSupervisor extends EventEmitter {
22
22
  restartBackoffMs = 5000;
23
23
  isRunning = false;
24
24
  lastHealthy = 0;
25
+ startupGraceMs = 20_000; // 20 seconds grace period after spawn before health checks
26
+ spawnedAt = 0;
25
27
  constructor(options) {
26
28
  super();
27
29
  this.projectDir = options.projectDir;
@@ -95,7 +97,7 @@ export class ServerSupervisor extends EventEmitter {
95
97
  return false;
96
98
  try {
97
99
  // Get the instar CLI path
98
- const cliPath = new URL('../../cli.js', import.meta.url).pathname;
100
+ const cliPath = new URL('../cli.js', import.meta.url).pathname;
99
101
  // --no-telegram: lifeline owns the Telegram connection, server should not poll
100
102
  const nodeCmd = ['node', cliPath, 'server', 'start', '--foreground', '--no-telegram']
101
103
  .map(arg => `'${arg.replace(/'/g, "'\\''")}'`)
@@ -108,6 +110,7 @@ export class ServerSupervisor extends EventEmitter {
108
110
  ], { stdio: 'ignore' });
109
111
  console.log(`[Supervisor] Server started in tmux session: ${this.serverSessionName}`);
110
112
  this.isRunning = true;
113
+ this.spawnedAt = Date.now();
111
114
  this.startHealthChecks();
112
115
  return true;
113
116
  }
@@ -133,6 +136,10 @@ export class ServerSupervisor extends EventEmitter {
133
136
  if (this.healthCheckInterval)
134
137
  return;
135
138
  this.healthCheckInterval = setInterval(async () => {
139
+ // Skip health checks during startup grace period — server needs time to boot
140
+ if (this.spawnedAt > 0 && (Date.now() - this.spawnedAt) < this.startupGraceMs) {
141
+ return;
142
+ }
136
143
  try {
137
144
  const healthy = await this.checkHealth();
138
145
  if (healthy) {
@@ -30,6 +30,7 @@ export declare class TelegramLifeline {
30
30
  private stopHeartbeat;
31
31
  private replayInterval;
32
32
  private lifelineTopicId;
33
+ private lockPath;
33
34
  constructor(projectDir?: string);
34
35
  /**
35
36
  * Start the lifeline — begins Telegram polling and server supervision.
@@ -44,6 +45,10 @@ export declare class TelegramLifeline {
44
45
  private handleLifelineCommand;
45
46
  private replayQueue;
46
47
  private notifyServerDown;
48
+ /**
49
+ * Check if OS-level autostart is installed for this project.
50
+ */
51
+ private isAutostartInstalled;
47
52
  /**
48
53
  * Ensure the Lifeline topic exists. Recreates if deleted.
49
54
  */