instar 0.7.44 → 0.7.46

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.
@@ -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.
@@ -794,6 +813,27 @@ export async function startServer(options) {
794
813
  console.log(pc.yellow(` Server running locally without tunnel. Fix tunnel config and restart.`));
795
814
  }
796
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
+ }
797
837
  // Graceful shutdown
798
838
  const shutdown = async () => {
799
839
  console.log('\nShutting down...');
@@ -813,10 +813,7 @@ ${argsXml}
813
813
  <key>RunAtLoad</key>
814
814
  <true/>
815
815
  <key>KeepAlive</key>
816
- <dict>
817
- <key>SuccessfulExit</key>
818
- <false/>
819
- </dict>
816
+ <true/>
820
817
  <key>StandardOutPath</key>
821
818
  <string>${escapeXml(path.join(logDir, `${command}-launchd.log`))}</string>
822
819
  <key>StandardErrorPath</key>
@@ -97,7 +97,7 @@ export class ServerSupervisor extends EventEmitter {
97
97
  return false;
98
98
  try {
99
99
  // Get the instar CLI path
100
- const cliPath = new URL('../../cli.js', import.meta.url).pathname;
100
+ const cliPath = new URL('../cli.js', import.meta.url).pathname;
101
101
  // --no-telegram: lifeline owns the Telegram connection, server should not poll
102
102
  const nodeCmd = ['node', cliPath, 'server', 'start', '--foreground', '--no-telegram']
103
103
  .map(arg => `'${arg.replace(/'/g, "'\\''")}'`)
@@ -45,6 +45,10 @@ export declare class TelegramLifeline {
45
45
  private handleLifelineCommand;
46
46
  private replayQueue;
47
47
  private notifyServerDown;
48
+ /**
49
+ * Check if OS-level autostart is installed for this project.
50
+ */
51
+ private isAutostartInstalled;
48
52
  /**
49
53
  * Ensure the Lifeline topic exists. Recreates if deleted.
50
54
  */
@@ -19,10 +19,12 @@
19
19
  * the full server crashes, runs out of memory, or gets stuck.
20
20
  */
21
21
  import fs from 'node:fs';
22
+ import os from 'node:os';
22
23
  import path from 'node:path';
23
24
  import pc from 'picocolors';
24
25
  import { loadConfig, ensureStateDir } from '../core/Config.js';
25
26
  import { registerPort, unregisterPort, startHeartbeat } from '../core/PortRegistry.js';
27
+ import { installAutoStart } from '../commands/setup.js';
26
28
  import { MessageQueue } from './MessageQueue.js';
27
29
  import { ServerSupervisor } from './ServerSupervisor.js';
28
30
  /**
@@ -127,7 +129,7 @@ export class TelegramLifeline {
127
129
  // Acquire exclusive lock — prevent multiple lifeline instances
128
130
  if (!acquireLockFile(this.lockPath)) {
129
131
  console.error(pc.red('[Lifeline] Another lifeline instance is already running. Exiting.'));
130
- process.exit(0); // Clean exit — launchd won't respawn on clean exit with KeepAlive config
132
+ process.exit(0); // Clean exit — launchd will restart after ThrottleInterval, acting as a watchdog
131
133
  }
132
134
  // Register in port registry (lifeline owns the port claim)
133
135
  try {
@@ -166,6 +168,19 @@ export class TelegramLifeline {
166
168
  setTimeout(() => this.replayQueue(), 5000); // Wait for server to fully start
167
169
  }
168
170
  }
171
+ // Self-healing: ensure autostart is installed so the lifeline persists across reboots.
172
+ // The user must always be able to reach their agent remotely — this is non-negotiable.
173
+ try {
174
+ if (!this.isAutostartInstalled()) {
175
+ const installed = installAutoStart(this.projectConfig.projectName, this.projectConfig.projectDir, true);
176
+ if (installed) {
177
+ console.log(pc.green(` Auto-start self-healed: installed ${process.platform === 'darwin' ? 'LaunchAgent' : 'systemd service'}`));
178
+ }
179
+ }
180
+ }
181
+ catch {
182
+ // Non-critical — don't crash the lifeline over autostart
183
+ }
169
184
  // Graceful shutdown
170
185
  const shutdown = async () => {
171
186
  console.log('\nLifeline shutting down...');
@@ -366,6 +381,22 @@ export class TelegramLifeline {
366
381
  await this.sendToTopic(topicId, `Server went down: ${reason}\n\nYour messages will be queued until recovery. Use /lifeline status to check.`).catch(() => { });
367
382
  }
368
383
  // ── Lifeline Topic ──────────────────────────────────────────
384
+ /**
385
+ * Check if OS-level autostart is installed for this project.
386
+ */
387
+ isAutostartInstalled() {
388
+ if (process.platform === 'darwin') {
389
+ const label = `ai.instar.${this.projectConfig.projectName}`;
390
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
391
+ return fs.existsSync(plistPath);
392
+ }
393
+ else if (process.platform === 'linux') {
394
+ const serviceName = `instar-${this.projectConfig.projectName}.service`;
395
+ const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', serviceName);
396
+ return fs.existsSync(servicePath);
397
+ }
398
+ return false;
399
+ }
369
400
  /**
370
401
  * Ensure the Lifeline topic exists. Recreates if deleted.
371
402
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.7.44",
3
+ "version": "0.7.46",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",