instar 0.7.21 → 0.7.23
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/.claude/skills/setup-wizard/skill.md +96 -2
- package/dist/commands/init.js +7 -7
- package/dist/commands/server.js +11 -3
- package/dist/commands/setup.js +2 -2
- package/dist/core/AutoUpdater.d.ts +114 -0
- package/dist/core/AutoUpdater.js +280 -0
- package/dist/core/SessionManager.js +13 -6
- package/dist/scaffold/templates.js +3 -3
- package/dist/scheduler/JobScheduler.js +6 -0
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +36 -0
- package/package.json +1 -1
|
@@ -250,6 +250,31 @@ This project uses instar for persistent agent capabilities.
|
|
|
250
250
|
- **Research before escalating** — Check tools first. Build solutions. "Needs human" is last resort.
|
|
251
251
|
```
|
|
252
252
|
|
|
253
|
+
**If Telegram was configured**, also add a Telegram Relay section to CLAUDE.md:
|
|
254
|
+
|
|
255
|
+
```markdown
|
|
256
|
+
## Telegram Relay
|
|
257
|
+
|
|
258
|
+
When user input starts with `[telegram:N]` (e.g., `[telegram:26] hello`), the message came via Telegram topic N.
|
|
259
|
+
|
|
260
|
+
**IMMEDIATE ACKNOWLEDGMENT (MANDATORY):** When you receive a Telegram message, your FIRST action must be sending a brief acknowledgment back. This confirms the message was received. Examples: "Got it, looking into this now." / "On it." Then do the work, then send the full response.
|
|
261
|
+
|
|
262
|
+
**Response relay:** After completing your work, relay your response back:
|
|
263
|
+
|
|
264
|
+
\`\`\`bash
|
|
265
|
+
cat <<'EOF' | .claude/scripts/telegram-reply.sh N
|
|
266
|
+
Your response text here
|
|
267
|
+
EOF
|
|
268
|
+
\`\`\`
|
|
269
|
+
|
|
270
|
+
Or for short messages:
|
|
271
|
+
\`\`\`bash
|
|
272
|
+
.claude/scripts/telegram-reply.sh N "Your response text here"
|
|
273
|
+
\`\`\`
|
|
274
|
+
|
|
275
|
+
Strip the `[telegram:N]` prefix before interpreting the message. Respond naturally, then relay. Only relay your conversational text — not tool output or internal reasoning.
|
|
276
|
+
```
|
|
277
|
+
|
|
253
278
|
## Phase 3: Telegram Setup — The Destination
|
|
254
279
|
|
|
255
280
|
**Telegram comes BEFORE technical configuration.** It's the whole point — everything else supports getting the user onto Telegram.
|
|
@@ -557,7 +582,7 @@ Create the directory structure and write config files:
|
|
|
557
582
|
mkdir -p .instar/state/sessions .instar/state/jobs .instar/logs
|
|
558
583
|
```
|
|
559
584
|
|
|
560
|
-
**`.instar/config.json
|
|
585
|
+
**`.instar/config.json`** (messaging section shown with Telegram — use `"messaging": []` if Telegram was not configured):
|
|
561
586
|
```json
|
|
562
587
|
{
|
|
563
588
|
"projectName": "my-project",
|
|
@@ -581,7 +606,19 @@ mkdir -p .instar/state/sessions .instar/state/jobs .instar/logs
|
|
|
581
606
|
"quotaThresholds": { "normal": 50, "elevated": 70, "critical": 85, "shutdown": 95 }
|
|
582
607
|
},
|
|
583
608
|
"users": [],
|
|
584
|
-
"messaging": [
|
|
609
|
+
"messaging": [
|
|
610
|
+
{
|
|
611
|
+
"type": "telegram",
|
|
612
|
+
"enabled": true,
|
|
613
|
+
"config": {
|
|
614
|
+
"token": "<BOT_TOKEN from BotFather>",
|
|
615
|
+
"chatId": "<CHAT_ID from Step 3e>",
|
|
616
|
+
"lifelineTopicId": "<LIFELINE_THREAD_ID from Step 3e>",
|
|
617
|
+
"pollIntervalMs": 2000,
|
|
618
|
+
"stallTimeoutMinutes": 5
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
],
|
|
585
622
|
"monitoring": {
|
|
586
623
|
"quotaTracking": false,
|
|
587
624
|
"memoryMonitoring": true,
|
|
@@ -603,6 +640,63 @@ Append if not present:
|
|
|
603
640
|
.instar/logs/
|
|
604
641
|
```
|
|
605
642
|
|
|
643
|
+
### 4f. Install Telegram Relay Script (if Telegram configured)
|
|
644
|
+
|
|
645
|
+
If Telegram was set up in Phase 3, install the relay script that lets Claude sessions send messages back to Telegram:
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
mkdir -p .claude/scripts
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Write `.claude/scripts/telegram-reply.sh`:
|
|
652
|
+
|
|
653
|
+
```bash
|
|
654
|
+
#!/bin/bash
|
|
655
|
+
# telegram-reply.sh — Send a message back to a Telegram topic via instar server.
|
|
656
|
+
#
|
|
657
|
+
# Usage:
|
|
658
|
+
# .claude/scripts/telegram-reply.sh TOPIC_ID "message text"
|
|
659
|
+
# echo "message text" | .claude/scripts/telegram-reply.sh TOPIC_ID
|
|
660
|
+
# cat <<'EOF' | .claude/scripts/telegram-reply.sh TOPIC_ID
|
|
661
|
+
# Multi-line message here
|
|
662
|
+
# EOF
|
|
663
|
+
|
|
664
|
+
TOPIC_ID=$1
|
|
665
|
+
shift
|
|
666
|
+
|
|
667
|
+
if [ -z "$TOPIC_ID" ]; then
|
|
668
|
+
echo "Usage: telegram-reply.sh TOPIC_ID [message]" >&2
|
|
669
|
+
exit 1
|
|
670
|
+
fi
|
|
671
|
+
|
|
672
|
+
PORT=<PORT>
|
|
673
|
+
|
|
674
|
+
# Get message from args or stdin
|
|
675
|
+
if [ $# -gt 0 ]; then
|
|
676
|
+
MESSAGE="$*"
|
|
677
|
+
else
|
|
678
|
+
MESSAGE=$(cat)
|
|
679
|
+
fi
|
|
680
|
+
|
|
681
|
+
if [ -z "$MESSAGE" ]; then
|
|
682
|
+
echo "No message provided" >&2
|
|
683
|
+
exit 1
|
|
684
|
+
fi
|
|
685
|
+
|
|
686
|
+
# Send via instar server API
|
|
687
|
+
curl -s -X POST "http://localhost:${PORT}/telegram/topic/${TOPIC_ID}/send" \
|
|
688
|
+
-H 'Content-Type: application/json' \
|
|
689
|
+
-d "$(jq -n --arg text "$MESSAGE" '{text: $text}')" > /dev/null 2>&1
|
|
690
|
+
|
|
691
|
+
echo "Sent $(echo "$MESSAGE" | wc -c | tr -d ' ') chars to topic $TOPIC_ID"
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Replace `<PORT>` with the actual server port. Then make it executable:
|
|
695
|
+
|
|
696
|
+
```bash
|
|
697
|
+
chmod +x .claude/scripts/telegram-reply.sh
|
|
698
|
+
```
|
|
699
|
+
|
|
606
700
|
## Phase 5: Launch & Handoff
|
|
607
701
|
|
|
608
702
|
**Do NOT ask "want me to start the server?" — just start it.** There is no reason not to. The whole point of setup is to get the agent running.
|
package/dist/commands/init.js
CHANGED
|
@@ -594,9 +594,9 @@ Instar has a built-in feedback loop. When something isn't working, your user can
|
|
|
594
594
|
- User complains to you naturally about an issue or missing capability
|
|
595
595
|
- You route the feedback to the Instar maintainers with context
|
|
596
596
|
- A fix or improvement is published as a new npm version
|
|
597
|
-
- The
|
|
597
|
+
- The built-in auto-updater detects the new version, applies it, notifies your user, and restarts the server
|
|
598
598
|
|
|
599
|
-
**A rising tide lifts all ships** — every user's feedback improves the platform for everyone.
|
|
599
|
+
**A rising tide lifts all ships** — every user's feedback improves the platform for everyone. Updates are applied automatically.
|
|
600
600
|
|
|
601
601
|
- Report: \`curl -X POST http://localhost:${port}/feedback -d '{"issue":"description","context":"relevant logs"}'\`
|
|
602
602
|
- Check updates: \`npm outdated -g instar\`
|
|
@@ -930,7 +930,7 @@ curl -s -X POST http://localhost:${port}/feedback/retry
|
|
|
930
930
|
|
|
931
931
|
## How It Works
|
|
932
932
|
|
|
933
|
-
Your feedback is stored locally AND forwarded to the instar maintainers. When they fix the issue and publish an update,
|
|
933
|
+
Your feedback is stored locally AND forwarded to the instar maintainers. When they fix the issue and publish an update, the built-in auto-updater detects it, applies it, and restarts the server — no manual intervention needed. One agent's bug report lifts all ships.
|
|
934
934
|
|
|
935
935
|
**User feedback matters too.** When your user says "this isn't working" or "I wish I could..." — capture it with their original words. User language carries context that technical rephrasing loses.
|
|
936
936
|
`,
|
|
@@ -995,16 +995,16 @@ function getDefaultJobs(port) {
|
|
|
995
995
|
{
|
|
996
996
|
slug: 'update-check',
|
|
997
997
|
name: 'Update Check',
|
|
998
|
-
description: '
|
|
998
|
+
description: 'Legacy update check job — disabled because the built-in AutoUpdater handles updates automatically (check, apply, notify, restart). See GET /updates/auto for status.',
|
|
999
999
|
schedule: '*/30 * * * *',
|
|
1000
1000
|
priority: 'medium',
|
|
1001
1001
|
expectedDurationMinutes: 2,
|
|
1002
1002
|
model: 'haiku',
|
|
1003
|
-
enabled:
|
|
1003
|
+
enabled: false,
|
|
1004
1004
|
gate: `curl -sf http://localhost:${port}/updates 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('updateAvailable') else 1)"`,
|
|
1005
1005
|
execute: {
|
|
1006
|
-
type: '
|
|
1007
|
-
value: `
|
|
1006
|
+
type: 'script',
|
|
1007
|
+
value: `curl -s http://localhost:${port}/updates/auto`,
|
|
1008
1008
|
},
|
|
1009
1009
|
tags: ['coherence', 'default'],
|
|
1010
1010
|
},
|
package/dist/commands/server.js
CHANGED
|
@@ -23,6 +23,7 @@ import { AnthropicIntelligenceProvider } from '../core/AnthropicIntelligenceProv
|
|
|
23
23
|
import { FeedbackManager } from '../core/FeedbackManager.js';
|
|
24
24
|
import { DispatchManager } from '../core/DispatchManager.js';
|
|
25
25
|
import { UpdateChecker } from '../core/UpdateChecker.js';
|
|
26
|
+
import { AutoUpdater } from '../core/AutoUpdater.js';
|
|
26
27
|
import { registerPort, unregisterPort, startHeartbeat } from '../core/PortRegistry.js';
|
|
27
28
|
import { TelegraphService } from '../publishing/TelegraphService.js';
|
|
28
29
|
import { PrivateViewer } from '../publishing/PrivateViewer.js';
|
|
@@ -459,16 +460,22 @@ export async function startServer(options) {
|
|
|
459
460
|
hasTelegram: config.messaging.some(m => m.type === 'telegram' && m.enabled),
|
|
460
461
|
projectName: config.projectName,
|
|
461
462
|
});
|
|
462
|
-
// Check for updates on startup
|
|
463
|
+
// Check for updates on startup (non-blocking)
|
|
463
464
|
updateChecker.check().then(info => {
|
|
464
465
|
if (info.updateAvailable) {
|
|
465
466
|
console.log(pc.yellow(` Update available: ${info.currentVersion} → ${info.latestVersion}`));
|
|
466
|
-
console.log(pc.yellow(` Run: npm update -g instar`));
|
|
467
467
|
}
|
|
468
468
|
else {
|
|
469
469
|
console.log(pc.green(` Instar ${info.currentVersion} is up to date`));
|
|
470
470
|
}
|
|
471
471
|
}).catch(() => { });
|
|
472
|
+
// Start auto-updater — periodic check + auto-apply + notify + restart
|
|
473
|
+
const autoUpdater = new AutoUpdater(updateChecker, state, config.stateDir, {
|
|
474
|
+
checkIntervalMinutes: 30,
|
|
475
|
+
autoApply: config.updates?.autoApply ?? true,
|
|
476
|
+
autoRestart: true,
|
|
477
|
+
}, telegram);
|
|
478
|
+
autoUpdater.start();
|
|
472
479
|
// Set up Telegraph publishing (auto-enabled when config exists or Telegram is configured)
|
|
473
480
|
let publisher;
|
|
474
481
|
const pubConfig = config.publishing;
|
|
@@ -503,7 +510,7 @@ export async function startServer(options) {
|
|
|
503
510
|
...(config.evolution || {}),
|
|
504
511
|
});
|
|
505
512
|
console.log(pc.green(' Evolution system enabled'));
|
|
506
|
-
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, quotaTracker, publisher, viewer, tunnel, evolution });
|
|
513
|
+
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, autoUpdater, quotaTracker, publisher, viewer, tunnel, evolution });
|
|
507
514
|
await server.start();
|
|
508
515
|
// Start tunnel AFTER server is listening
|
|
509
516
|
if (tunnel) {
|
|
@@ -519,6 +526,7 @@ export async function startServer(options) {
|
|
|
519
526
|
// Graceful shutdown
|
|
520
527
|
const shutdown = async () => {
|
|
521
528
|
console.log('\nShutting down...');
|
|
529
|
+
autoUpdater.stop();
|
|
522
530
|
if (tunnel)
|
|
523
531
|
await tunnel.stop();
|
|
524
532
|
stopHeartbeat();
|
package/dist/commands/setup.js
CHANGED
|
@@ -1300,9 +1300,9 @@ Instar has a built-in feedback loop — a rising tide that lifts all ships. When
|
|
|
1300
1300
|
- User complains naturally about an issue or missing capability
|
|
1301
1301
|
- Agent packages the issue with context and routes it upstream
|
|
1302
1302
|
- A fix is published as a new npm version
|
|
1303
|
-
- The
|
|
1303
|
+
- The built-in auto-updater detects the new version, applies it, notifies the user, and restarts the server
|
|
1304
1304
|
|
|
1305
|
-
Every user's feedback makes the platform better for everyone. Report issues when you encounter them.
|
|
1305
|
+
Every user's feedback makes the platform better for everyone. Report issues when you encounter them. Updates are applied automatically — check status with \`curl http://localhost:PORT/updates/auto\`.
|
|
1306
1306
|
|
|
1307
1307
|
### Self-Evolution
|
|
1308
1308
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto Updater — built-in periodic update mechanism.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the server process (no Claude session needed).
|
|
5
|
+
* Periodically checks for updates, auto-applies when available,
|
|
6
|
+
* notifies via Telegram, and handles server restart.
|
|
7
|
+
*
|
|
8
|
+
* This replaces the heavyweight prompt-based update-check job.
|
|
9
|
+
* Updates should never depend on the job scheduler — they're
|
|
10
|
+
* core infrastructure that must run independently.
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* check → apply → migrate → notify → restart
|
|
14
|
+
*
|
|
15
|
+
* Restart strategy:
|
|
16
|
+
* After npm update replaces the CLI on disk, spawn a replacement
|
|
17
|
+
* server process and exit. The new process binds to the port after
|
|
18
|
+
* the old one releases it during shutdown.
|
|
19
|
+
*/
|
|
20
|
+
import type { UpdateChecker } from './UpdateChecker.js';
|
|
21
|
+
import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
|
|
22
|
+
import type { StateManager } from './StateManager.js';
|
|
23
|
+
export interface AutoUpdaterConfig {
|
|
24
|
+
/** How often to check for updates, in minutes. Default: 30 */
|
|
25
|
+
checkIntervalMinutes?: number;
|
|
26
|
+
/** Whether to auto-apply updates. Default: true */
|
|
27
|
+
autoApply?: boolean;
|
|
28
|
+
/** Telegram topic ID for update notifications (uses Agent Attention if not set) */
|
|
29
|
+
notificationTopicId?: number;
|
|
30
|
+
/** Whether to auto-restart after applying an update. Default: true */
|
|
31
|
+
autoRestart?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface AutoUpdaterStatus {
|
|
34
|
+
/** Whether the auto-updater is running */
|
|
35
|
+
running: boolean;
|
|
36
|
+
/** Last time we checked for updates */
|
|
37
|
+
lastCheck: string | null;
|
|
38
|
+
/** Last time we applied an update */
|
|
39
|
+
lastApply: string | null;
|
|
40
|
+
/** Current configuration */
|
|
41
|
+
config: Required<AutoUpdaterConfig>;
|
|
42
|
+
/** Any pending update that hasn't been applied yet */
|
|
43
|
+
pendingUpdate: string | null;
|
|
44
|
+
/** Last error if any */
|
|
45
|
+
lastError: string | null;
|
|
46
|
+
}
|
|
47
|
+
export declare class AutoUpdater {
|
|
48
|
+
private updateChecker;
|
|
49
|
+
private telegram;
|
|
50
|
+
private state;
|
|
51
|
+
private config;
|
|
52
|
+
private interval;
|
|
53
|
+
private lastCheck;
|
|
54
|
+
private lastApply;
|
|
55
|
+
private lastError;
|
|
56
|
+
private pendingUpdate;
|
|
57
|
+
private isApplying;
|
|
58
|
+
private stateFile;
|
|
59
|
+
constructor(updateChecker: UpdateChecker, state: StateManager, stateDir: string, config?: AutoUpdaterConfig, telegram?: TelegramAdapter | null);
|
|
60
|
+
/**
|
|
61
|
+
* Start the periodic update checker.
|
|
62
|
+
* Idempotent — calling start() when already running is a no-op.
|
|
63
|
+
*/
|
|
64
|
+
start(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Stop the periodic checker.
|
|
67
|
+
*/
|
|
68
|
+
stop(): void;
|
|
69
|
+
/**
|
|
70
|
+
* Get current auto-updater status.
|
|
71
|
+
*/
|
|
72
|
+
getStatus(): AutoUpdaterStatus;
|
|
73
|
+
/**
|
|
74
|
+
* Set the Telegram adapter (may be wired after construction).
|
|
75
|
+
*/
|
|
76
|
+
setTelegram(telegram: TelegramAdapter): void;
|
|
77
|
+
/**
|
|
78
|
+
* One tick of the update loop.
|
|
79
|
+
* Check → optionally apply → notify → optionally restart.
|
|
80
|
+
*/
|
|
81
|
+
private tick;
|
|
82
|
+
/**
|
|
83
|
+
* Self-restart the server after an update.
|
|
84
|
+
*
|
|
85
|
+
* Strategy:
|
|
86
|
+
* 1. Spawn a shell that waits 2 seconds (for port release), then
|
|
87
|
+
* starts the new server version using the same CLI arguments.
|
|
88
|
+
* 2. Send SIGTERM to ourselves to trigger graceful shutdown.
|
|
89
|
+
*
|
|
90
|
+
* The 2-second delay ensures the old process has time to release
|
|
91
|
+
* the port before the new one tries to bind.
|
|
92
|
+
*
|
|
93
|
+
* If running in tmux, the replacement process inherits the PTY.
|
|
94
|
+
* If running under a process manager (launchd, systemd), the
|
|
95
|
+
* manager handles restart automatically after we exit.
|
|
96
|
+
*/
|
|
97
|
+
private selfRestart;
|
|
98
|
+
/**
|
|
99
|
+
* Send a notification via Telegram (if configured).
|
|
100
|
+
* Falls back to console logging if Telegram is not available.
|
|
101
|
+
*/
|
|
102
|
+
private notify;
|
|
103
|
+
/**
|
|
104
|
+
* Get the Agent Attention topic ID from state (where infrastructure notifications go).
|
|
105
|
+
*/
|
|
106
|
+
private getAttentionTopicId;
|
|
107
|
+
/**
|
|
108
|
+
* Get the server port from the update checker config (for notification messages).
|
|
109
|
+
*/
|
|
110
|
+
private getPort;
|
|
111
|
+
private loadState;
|
|
112
|
+
private saveState;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=AutoUpdater.d.ts.map
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto Updater — built-in periodic update mechanism.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the server process (no Claude session needed).
|
|
5
|
+
* Periodically checks for updates, auto-applies when available,
|
|
6
|
+
* notifies via Telegram, and handles server restart.
|
|
7
|
+
*
|
|
8
|
+
* This replaces the heavyweight prompt-based update-check job.
|
|
9
|
+
* Updates should never depend on the job scheduler — they're
|
|
10
|
+
* core infrastructure that must run independently.
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* check → apply → migrate → notify → restart
|
|
14
|
+
*
|
|
15
|
+
* Restart strategy:
|
|
16
|
+
* After npm update replaces the CLI on disk, spawn a replacement
|
|
17
|
+
* server process and exit. The new process binds to the port after
|
|
18
|
+
* the old one releases it during shutdown.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
export class AutoUpdater {
|
|
24
|
+
updateChecker;
|
|
25
|
+
telegram;
|
|
26
|
+
state;
|
|
27
|
+
config;
|
|
28
|
+
interval = null;
|
|
29
|
+
lastCheck = null;
|
|
30
|
+
lastApply = null;
|
|
31
|
+
lastError = null;
|
|
32
|
+
pendingUpdate = null;
|
|
33
|
+
isApplying = false;
|
|
34
|
+
stateFile;
|
|
35
|
+
constructor(updateChecker, state, stateDir, config, telegram) {
|
|
36
|
+
this.updateChecker = updateChecker;
|
|
37
|
+
this.state = state;
|
|
38
|
+
this.telegram = telegram ?? null;
|
|
39
|
+
this.stateFile = path.join(stateDir, 'state', 'auto-updater.json');
|
|
40
|
+
this.config = {
|
|
41
|
+
checkIntervalMinutes: config?.checkIntervalMinutes ?? 30,
|
|
42
|
+
autoApply: config?.autoApply ?? true,
|
|
43
|
+
autoRestart: config?.autoRestart ?? true,
|
|
44
|
+
notificationTopicId: config?.notificationTopicId ?? 0,
|
|
45
|
+
};
|
|
46
|
+
// Load persisted state (survives restarts)
|
|
47
|
+
this.loadState();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Start the periodic update checker.
|
|
51
|
+
* Idempotent — calling start() when already running is a no-op.
|
|
52
|
+
*/
|
|
53
|
+
start() {
|
|
54
|
+
if (this.interval)
|
|
55
|
+
return;
|
|
56
|
+
const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
|
|
57
|
+
console.log(`[AutoUpdater] Started (every ${this.config.checkIntervalMinutes}m, ` +
|
|
58
|
+
`autoApply: ${this.config.autoApply}, autoRestart: ${this.config.autoRestart})`);
|
|
59
|
+
// Run first check after a short delay (don't block startup)
|
|
60
|
+
setTimeout(() => this.tick(), 10_000);
|
|
61
|
+
// Then run periodically
|
|
62
|
+
this.interval = setInterval(() => this.tick(), intervalMs);
|
|
63
|
+
this.interval.unref(); // Don't prevent process exit
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stop the periodic checker.
|
|
67
|
+
*/
|
|
68
|
+
stop() {
|
|
69
|
+
if (this.interval) {
|
|
70
|
+
clearInterval(this.interval);
|
|
71
|
+
this.interval = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get current auto-updater status.
|
|
76
|
+
*/
|
|
77
|
+
getStatus() {
|
|
78
|
+
return {
|
|
79
|
+
running: this.interval !== null,
|
|
80
|
+
lastCheck: this.lastCheck,
|
|
81
|
+
lastApply: this.lastApply,
|
|
82
|
+
config: { ...this.config },
|
|
83
|
+
pendingUpdate: this.pendingUpdate,
|
|
84
|
+
lastError: this.lastError,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Set the Telegram adapter (may be wired after construction).
|
|
89
|
+
*/
|
|
90
|
+
setTelegram(telegram) {
|
|
91
|
+
this.telegram = telegram;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* One tick of the update loop.
|
|
95
|
+
* Check → optionally apply → notify → optionally restart.
|
|
96
|
+
*/
|
|
97
|
+
async tick() {
|
|
98
|
+
if (this.isApplying) {
|
|
99
|
+
console.log('[AutoUpdater] Skipping tick — update already in progress');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
// Step 1: Check for updates
|
|
104
|
+
const info = await this.updateChecker.check();
|
|
105
|
+
this.lastCheck = new Date().toISOString();
|
|
106
|
+
this.lastError = null;
|
|
107
|
+
if (!info.updateAvailable) {
|
|
108
|
+
this.pendingUpdate = null;
|
|
109
|
+
this.saveState();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(`[AutoUpdater] Update available: ${info.currentVersion} → ${info.latestVersion}`);
|
|
113
|
+
this.pendingUpdate = info.latestVersion;
|
|
114
|
+
this.saveState();
|
|
115
|
+
// Step 2: Auto-apply if configured
|
|
116
|
+
if (!this.config.autoApply) {
|
|
117
|
+
// Just notify — don't apply
|
|
118
|
+
await this.notify(`Update available: v${info.currentVersion} → v${info.latestVersion}\n\n` +
|
|
119
|
+
(info.changeSummary ? `Changes: ${info.changeSummary}\n\n` : '') +
|
|
120
|
+
`Auto-apply is disabled. Apply manually:\n` +
|
|
121
|
+
`curl -X POST http://localhost:${this.getPort()}/updates/apply`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Step 3: Apply the update
|
|
125
|
+
this.isApplying = true;
|
|
126
|
+
console.log(`[AutoUpdater] Applying update to v${info.latestVersion}...`);
|
|
127
|
+
const result = await this.updateChecker.applyUpdate();
|
|
128
|
+
this.isApplying = false;
|
|
129
|
+
if (!result.success) {
|
|
130
|
+
this.lastError = result.message;
|
|
131
|
+
this.saveState();
|
|
132
|
+
console.error(`[AutoUpdater] Update failed: ${result.message}`);
|
|
133
|
+
await this.notify(`Update to v${info.latestVersion} failed: ${result.message}\n\n` +
|
|
134
|
+
`The current version (v${result.previousVersion}) is still running.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Step 4: Update succeeded
|
|
138
|
+
this.lastApply = new Date().toISOString();
|
|
139
|
+
this.pendingUpdate = null;
|
|
140
|
+
this.saveState();
|
|
141
|
+
console.log(`[AutoUpdater] Updated: v${result.previousVersion} → v${result.newVersion}`);
|
|
142
|
+
// Step 5: Notify via Telegram
|
|
143
|
+
const restartNote = result.restartNeeded && this.config.autoRestart
|
|
144
|
+
? 'Server is restarting now...'
|
|
145
|
+
: result.restartNeeded
|
|
146
|
+
? 'A server restart is needed to use the new version.'
|
|
147
|
+
: '';
|
|
148
|
+
await this.notify(`Updated: v${result.previousVersion} → v${result.newVersion}\n\n` +
|
|
149
|
+
(info.changeSummary ? `What changed:\n${info.changeSummary}\n\n` : '') +
|
|
150
|
+
restartNote);
|
|
151
|
+
// Step 6: Self-restart if needed and configured
|
|
152
|
+
if (result.restartNeeded && this.config.autoRestart) {
|
|
153
|
+
// Brief delay to let the Telegram notification send
|
|
154
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
155
|
+
this.selfRestart();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
this.isApplying = false;
|
|
160
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
161
|
+
this.saveState();
|
|
162
|
+
console.error(`[AutoUpdater] Tick error: ${this.lastError}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Self-restart the server after an update.
|
|
167
|
+
*
|
|
168
|
+
* Strategy:
|
|
169
|
+
* 1. Spawn a shell that waits 2 seconds (for port release), then
|
|
170
|
+
* starts the new server version using the same CLI arguments.
|
|
171
|
+
* 2. Send SIGTERM to ourselves to trigger graceful shutdown.
|
|
172
|
+
*
|
|
173
|
+
* The 2-second delay ensures the old process has time to release
|
|
174
|
+
* the port before the new one tries to bind.
|
|
175
|
+
*
|
|
176
|
+
* If running in tmux, the replacement process inherits the PTY.
|
|
177
|
+
* If running under a process manager (launchd, systemd), the
|
|
178
|
+
* manager handles restart automatically after we exit.
|
|
179
|
+
*/
|
|
180
|
+
selfRestart() {
|
|
181
|
+
console.log('[AutoUpdater] Initiating self-restart...');
|
|
182
|
+
// Build the command to restart
|
|
183
|
+
// process.argv[0] = node binary
|
|
184
|
+
// process.argv[1..] = CLI args (e.g., /path/to/cli.js server start --foreground)
|
|
185
|
+
const args = process.argv.slice(1)
|
|
186
|
+
.map(a => `'${a.replace(/'/g, "'\\''")}'`)
|
|
187
|
+
.join(' ');
|
|
188
|
+
const cmd = `sleep 2 && exec ${process.execPath} ${args}`;
|
|
189
|
+
try {
|
|
190
|
+
const child = spawn('sh', ['-c', cmd], {
|
|
191
|
+
detached: true,
|
|
192
|
+
stdio: 'inherit',
|
|
193
|
+
cwd: process.cwd(),
|
|
194
|
+
env: process.env,
|
|
195
|
+
});
|
|
196
|
+
child.unref();
|
|
197
|
+
console.log('[AutoUpdater] Replacement process spawned. Shutting down...');
|
|
198
|
+
// Trigger graceful shutdown (the SIGTERM handler in server.ts will clean up)
|
|
199
|
+
process.kill(process.pid, 'SIGTERM');
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.error(`[AutoUpdater] Self-restart failed: ${err}`);
|
|
203
|
+
console.error('[AutoUpdater] Update was applied but manual restart is needed.');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Send a notification via Telegram (if configured).
|
|
208
|
+
* Falls back to console logging if Telegram is not available.
|
|
209
|
+
*/
|
|
210
|
+
async notify(message) {
|
|
211
|
+
const formatted = `🔄 *Auto-Update*\n\n${message}`;
|
|
212
|
+
if (this.telegram) {
|
|
213
|
+
try {
|
|
214
|
+
const topicId = this.config.notificationTopicId || this.getAttentionTopicId();
|
|
215
|
+
if (topicId) {
|
|
216
|
+
await this.telegram.sendToTopic(topicId, formatted);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.error(`[AutoUpdater] Telegram notification failed: ${err}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Fallback: just log
|
|
225
|
+
console.log(`[AutoUpdater] Notification: ${message}`);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get the Agent Attention topic ID from state (where infrastructure notifications go).
|
|
229
|
+
*/
|
|
230
|
+
getAttentionTopicId() {
|
|
231
|
+
return this.state.get('agent-attention-topic') ?? 0;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get the server port from the update checker config (for notification messages).
|
|
235
|
+
*/
|
|
236
|
+
getPort() {
|
|
237
|
+
// The port is available on the UpdateChecker config but not exposed.
|
|
238
|
+
// Use a reasonable default — agents can find their port from config.
|
|
239
|
+
return 4040;
|
|
240
|
+
}
|
|
241
|
+
// ── State persistence ──────────────────────────────────────────────
|
|
242
|
+
loadState() {
|
|
243
|
+
try {
|
|
244
|
+
if (fs.existsSync(this.stateFile)) {
|
|
245
|
+
const data = JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
|
|
246
|
+
this.lastCheck = data.lastCheck ?? null;
|
|
247
|
+
this.lastApply = data.lastApply ?? null;
|
|
248
|
+
this.lastError = data.lastError ?? null;
|
|
249
|
+
this.pendingUpdate = data.pendingUpdate ?? null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Start fresh if state is corrupted
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
saveState() {
|
|
257
|
+
const dir = path.dirname(this.stateFile);
|
|
258
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
259
|
+
const data = {
|
|
260
|
+
lastCheck: this.lastCheck,
|
|
261
|
+
lastApply: this.lastApply,
|
|
262
|
+
lastError: this.lastError,
|
|
263
|
+
pendingUpdate: this.pendingUpdate,
|
|
264
|
+
savedAt: new Date().toISOString(),
|
|
265
|
+
};
|
|
266
|
+
// Atomic write
|
|
267
|
+
const tmpPath = this.stateFile + `.${process.pid}.tmp`;
|
|
268
|
+
try {
|
|
269
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
270
|
+
fs.renameSync(tmpPath, this.stateFile);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
try {
|
|
274
|
+
fs.unlinkSync(tmpPath);
|
|
275
|
+
}
|
|
276
|
+
catch { /* ignore */ }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=AutoUpdater.js.map
|
|
@@ -127,20 +127,24 @@ export class SessionManager extends EventEmitter {
|
|
|
127
127
|
if (this.tmuxSessionExists(tmuxSession)) {
|
|
128
128
|
throw new Error(`tmux session "${tmuxSession}" already exists`);
|
|
129
129
|
}
|
|
130
|
-
// Build Claude CLI arguments
|
|
131
|
-
//
|
|
132
|
-
// when
|
|
130
|
+
// Build Claude CLI arguments via bash wrapper.
|
|
131
|
+
// Must unset CLAUDECODE to prevent "cannot be launched inside another Claude Code session"
|
|
132
|
+
// error when instar itself runs inside Claude Code.
|
|
133
133
|
const claudeArgs = ['--dangerously-skip-permissions'];
|
|
134
134
|
if (options.model) {
|
|
135
135
|
claudeArgs.push('--model', options.model);
|
|
136
136
|
}
|
|
137
137
|
claudeArgs.push('-p', options.prompt);
|
|
138
|
+
const claudeCmd = [this.config.claudePath, ...claudeArgs]
|
|
139
|
+
.map(a => a.replace(/'/g, "'\\''"))
|
|
140
|
+
.map(a => `'${a}'`)
|
|
141
|
+
.join(' ');
|
|
138
142
|
try {
|
|
139
143
|
execFileSync(this.config.tmuxPath, [
|
|
140
144
|
'new-session', '-d',
|
|
141
145
|
'-s', tmuxSession,
|
|
142
146
|
'-c', this.config.projectDir,
|
|
143
|
-
|
|
147
|
+
'bash', '-c', `unset CLAUDECODE; exec ${claudeCmd}`,
|
|
144
148
|
], { encoding: 'utf-8' });
|
|
145
149
|
}
|
|
146
150
|
catch (err) {
|
|
@@ -371,11 +375,14 @@ export class SessionManager extends EventEmitter {
|
|
|
371
375
|
];
|
|
372
376
|
if (options?.telegramTopicId) {
|
|
373
377
|
// Wrap in bash shell to export env var before Claude starts
|
|
378
|
+
// Also unset CLAUDECODE to prevent nested Claude Code errors
|
|
374
379
|
const claudeCmd = `${this.config.claudePath} --dangerously-skip-permissions`;
|
|
375
|
-
tmuxArgs.push('bash', '-c', `export INSTAR_TELEGRAM_TOPIC=${options.telegramTopicId} && exec ${claudeCmd}`);
|
|
380
|
+
tmuxArgs.push('bash', '-c', `unset CLAUDECODE; export INSTAR_TELEGRAM_TOPIC=${options.telegramTopicId} && exec ${claudeCmd}`);
|
|
376
381
|
}
|
|
377
382
|
else {
|
|
378
|
-
|
|
383
|
+
// Unset CLAUDECODE to prevent nested Claude Code errors
|
|
384
|
+
const claudeCmd = `${this.config.claudePath} --dangerously-skip-permissions`;
|
|
385
|
+
tmuxArgs.push('bash', '-c', `unset CLAUDECODE; exec ${claudeCmd}`);
|
|
379
386
|
}
|
|
380
387
|
execFileSync(this.config.tmuxPath, tmuxArgs, { encoding: 'utf-8' });
|
|
381
388
|
}
|
|
@@ -385,14 +385,14 @@ Types: \`bug\`, \`feature\`, \`improvement\`, \`question\`
|
|
|
385
385
|
|
|
386
386
|
### The Feedback Loop — Rising Tide
|
|
387
387
|
|
|
388
|
-
Instar has a built-in feedback loop that connects all agents to the development team (Dawn). When you submit feedback, it gets forwarded upstream. When a fix ships, the
|
|
388
|
+
Instar has a built-in feedback loop that connects all agents to the development team (Dawn). When you submit feedback, it gets forwarded upstream. When a fix ships, the built-in auto-updater detects it, applies it, and restarts the server. Every agent's bug report lifts all ships.
|
|
389
389
|
|
|
390
390
|
**The cycle:**
|
|
391
391
|
1. You encounter an issue → submit feedback
|
|
392
392
|
2. Feedback forwards upstream to Dawn
|
|
393
393
|
3. Dawn fixes and publishes a new version
|
|
394
|
-
4.
|
|
395
|
-
5. Update auto-applies, hooks refresh, you get the fix
|
|
394
|
+
4. The auto-updater detects it within 30 minutes
|
|
395
|
+
5. Update auto-applies, hooks refresh, server restarts, you get the fix
|
|
396
396
|
|
|
397
397
|
**User feedback matters too.** When your user says "this isn't working" or "I wish I could..." — that's feedback. Categorize it and submit it the same way.
|
|
398
398
|
|
|
@@ -406,6 +406,12 @@ export class JobScheduler {
|
|
|
406
406
|
else {
|
|
407
407
|
summary += '\n_No output captured (session already closed)_';
|
|
408
408
|
}
|
|
409
|
+
// Skip Telegram notification for successful jobs with no meaningful output
|
|
410
|
+
// Prevents empty notification spam (e.g., dispatch-check when dispatch is unconfigured)
|
|
411
|
+
if (!failed && (!output || !output.trim())) {
|
|
412
|
+
console.log(`[scheduler] Skipping notification for ${job.slug} — no meaningful output`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
409
415
|
// Send to the job's dedicated topic if available, otherwise fall back to generic messenger
|
|
410
416
|
if (this.telegram && job.topicId) {
|
|
411
417
|
try {
|
|
@@ -14,6 +14,7 @@ import type { RelationshipManager } from '../core/RelationshipManager.js';
|
|
|
14
14
|
import type { FeedbackManager } from '../core/FeedbackManager.js';
|
|
15
15
|
import type { DispatchManager } from '../core/DispatchManager.js';
|
|
16
16
|
import type { UpdateChecker } from '../core/UpdateChecker.js';
|
|
17
|
+
import type { AutoUpdater } from '../core/AutoUpdater.js';
|
|
17
18
|
import type { QuotaTracker } from '../monitoring/QuotaTracker.js';
|
|
18
19
|
import type { TelegraphService } from '../publishing/TelegraphService.js';
|
|
19
20
|
import type { PrivateViewer } from '../publishing/PrivateViewer.js';
|
|
@@ -34,6 +35,7 @@ export declare class AgentServer {
|
|
|
34
35
|
feedback?: FeedbackManager;
|
|
35
36
|
dispatches?: DispatchManager;
|
|
36
37
|
updateChecker?: UpdateChecker;
|
|
38
|
+
autoUpdater?: AutoUpdater;
|
|
37
39
|
quotaTracker?: QuotaTracker;
|
|
38
40
|
publisher?: TelegraphService;
|
|
39
41
|
viewer?: PrivateViewer;
|
|
@@ -32,6 +32,7 @@ export class AgentServer {
|
|
|
32
32
|
feedback: options.feedback ?? null,
|
|
33
33
|
dispatches: options.dispatches ?? null,
|
|
34
34
|
updateChecker: options.updateChecker ?? null,
|
|
35
|
+
autoUpdater: options.autoUpdater ?? null,
|
|
35
36
|
quotaTracker: options.quotaTracker ?? null,
|
|
36
37
|
publisher: options.publisher ?? null,
|
|
37
38
|
viewer: options.viewer ?? null,
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { RelationshipManager } from '../core/RelationshipManager.js';
|
|
|
14
14
|
import type { FeedbackManager } from '../core/FeedbackManager.js';
|
|
15
15
|
import type { DispatchManager } from '../core/DispatchManager.js';
|
|
16
16
|
import type { UpdateChecker } from '../core/UpdateChecker.js';
|
|
17
|
+
import type { AutoUpdater } from '../core/AutoUpdater.js';
|
|
17
18
|
import type { QuotaTracker } from '../monitoring/QuotaTracker.js';
|
|
18
19
|
import type { TelegraphService } from '../publishing/TelegraphService.js';
|
|
19
20
|
import type { PrivateViewer } from '../publishing/PrivateViewer.js';
|
|
@@ -29,6 +30,7 @@ export interface RouteContext {
|
|
|
29
30
|
feedback: FeedbackManager | null;
|
|
30
31
|
dispatches: DispatchManager | null;
|
|
31
32
|
updateChecker: UpdateChecker | null;
|
|
33
|
+
autoUpdater: AutoUpdater | null;
|
|
32
34
|
quotaTracker: QuotaTracker | null;
|
|
33
35
|
publisher: TelegraphService | null;
|
|
34
36
|
viewer: PrivateViewer | null;
|
package/dist/server/routes.js
CHANGED
|
@@ -476,6 +476,34 @@ export function createRoutes(ctx) {
|
|
|
476
476
|
}
|
|
477
477
|
res.json({ topics: ctx.telegram.getAllTopicMappings() });
|
|
478
478
|
});
|
|
479
|
+
router.post('/telegram/topics', async (req, res) => {
|
|
480
|
+
if (!ctx.telegram) {
|
|
481
|
+
res.status(503).json({ error: 'Telegram not configured' });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const { name, color } = req.body;
|
|
485
|
+
if (!name || typeof name !== 'string' || name.trim().length < 1) {
|
|
486
|
+
res.status(400).json({ error: '"name" is required (non-empty string)' });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (name.length > 128) {
|
|
490
|
+
res.status(400).json({ error: '"name" must be 128 characters or fewer' });
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
// Color is optional — defaults to green (9367192)
|
|
494
|
+
const iconColor = typeof color === 'number' ? color : 9367192;
|
|
495
|
+
try {
|
|
496
|
+
const topic = await ctx.telegram.createForumTopic(name.trim(), iconColor);
|
|
497
|
+
res.status(201).json({
|
|
498
|
+
topicId: topic.topicId,
|
|
499
|
+
name: name.trim(),
|
|
500
|
+
created: true,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
505
|
+
}
|
|
506
|
+
});
|
|
479
507
|
router.post('/telegram/reply/:topicId', async (req, res) => {
|
|
480
508
|
if (!ctx.telegram) {
|
|
481
509
|
res.status(503).json({ error: 'Telegram not configured' });
|
|
@@ -870,6 +898,14 @@ export function createRoutes(ctx) {
|
|
|
870
898
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
871
899
|
}
|
|
872
900
|
});
|
|
901
|
+
// ── Auto-Updater ────────────────────────────────────────────────
|
|
902
|
+
router.get('/updates/auto', (_req, res) => {
|
|
903
|
+
if (!ctx.autoUpdater) {
|
|
904
|
+
res.status(503).json({ error: 'Auto-updater not configured' });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
res.json(ctx.autoUpdater.getStatus());
|
|
908
|
+
});
|
|
873
909
|
// ── Dispatches ───────────────────────────────────────────────────
|
|
874
910
|
router.get('/dispatches', async (_req, res) => {
|
|
875
911
|
if (!ctx.dispatches) {
|