neoagent 2.1.16-beta.0 → 2.1.16
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/lib/manager.js +64 -6
- package/package.json +1 -1
- package/runtime/release_channel.js +126 -0
- package/server/db/database.js +2 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/routes/agents.js +15 -3
- package/server/routes/settings.js +21 -48
- package/server/services/ai/engine.js +38 -10
- package/server/services/ai/tools.js +14 -3
- package/server/services/messaging/automation.js +1 -1
- package/server/services/messaging/manager.js +12 -4
- package/server/utils/update_status.js +93 -0
- package/server/utils/version.js +4 -4
package/lib/manager.js
CHANGED
|
@@ -25,9 +25,11 @@ const {
|
|
|
25
25
|
parseReleaseChannel,
|
|
26
26
|
getReleaseChannelBranch,
|
|
27
27
|
getReleaseChannelDistTag,
|
|
28
|
-
getReleaseChannelLabel,
|
|
29
28
|
readConfiguredReleaseChannel,
|
|
30
29
|
writeReleaseChannelToEnvFile,
|
|
30
|
+
describeReleaseChannelPolicy,
|
|
31
|
+
choosePreferredBranchForChannel,
|
|
32
|
+
choosePreferredNpmTagForChannel,
|
|
31
33
|
} = require('../runtime/release_channel');
|
|
32
34
|
|
|
33
35
|
const APP_NAME = 'NeoAgent';
|
|
@@ -197,8 +199,7 @@ function currentReleaseChannel() {
|
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
function releaseChannelSummary(channel) {
|
|
200
|
-
|
|
201
|
-
return `${getReleaseChannelLabel(normalized)} (branch ${getReleaseChannelBranch(normalized)}, npm ${getReleaseChannelDistTag(normalized)})`;
|
|
202
|
+
return describeReleaseChannelPolicy(parseReleaseChannel(channel) || currentReleaseChannel());
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
function gitWorkingTreeDirty() {
|
|
@@ -214,6 +215,62 @@ function gitRemoteBranchExists(branch) {
|
|
|
214
215
|
return runQuiet('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch]).status === 0;
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
function latestGitTagVersion(pattern) {
|
|
219
|
+
const res = runQuiet('git', ['tag', '--list', pattern, '--sort=-v:refname']);
|
|
220
|
+
if (res.status !== 0) return null;
|
|
221
|
+
const tag = res.stdout
|
|
222
|
+
.split('\n')
|
|
223
|
+
.map((value) => value.trim())
|
|
224
|
+
.find(Boolean);
|
|
225
|
+
return tag ? tag.replace(/^v/, '') : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resolvePreferredGitBranch(channel) {
|
|
229
|
+
const normalized = parseReleaseChannel(channel) || currentReleaseChannel();
|
|
230
|
+
if (normalized === 'stable') {
|
|
231
|
+
return getReleaseChannelBranch(normalized);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const stableVersion = latestGitTagVersion('v[0-9]*.[0-9]*.[0-9]*');
|
|
235
|
+
const betaVersion = latestGitTagVersion('v[0-9]*.[0-9]*.[0-9]*-beta.*');
|
|
236
|
+
const preferred = choosePreferredBranchForChannel(normalized, {
|
|
237
|
+
stable: stableVersion,
|
|
238
|
+
beta: betaVersion,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (preferred === 'beta' && !gitRemoteBranchExists('beta')) {
|
|
242
|
+
return 'main';
|
|
243
|
+
}
|
|
244
|
+
return preferred;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function resolvePreferredNpmTag(channel) {
|
|
248
|
+
const normalized = parseReleaseChannel(channel) || currentReleaseChannel();
|
|
249
|
+
if (normalized === 'stable') {
|
|
250
|
+
return getReleaseChannelDistTag(normalized);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const distTags = {};
|
|
254
|
+
const tagsRes = runQuiet('npm', ['view', 'neoagent', 'dist-tags', '--json'], {
|
|
255
|
+
env: withInstallEnv(),
|
|
256
|
+
});
|
|
257
|
+
if (tagsRes.status === 0) {
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(tagsRes.stdout || '{}');
|
|
260
|
+
if (parsed && typeof parsed === 'object') {
|
|
261
|
+
Object.assign(distTags, parsed);
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// Ignore parse failures and fall back to the beta tag.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return choosePreferredNpmTagForChannel(normalized, {
|
|
269
|
+
latest: distTags.latest,
|
|
270
|
+
beta: distTags.beta,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
217
274
|
function ensureGitBranchForReleaseChannel(targetBranch) {
|
|
218
275
|
const branchRes = runQuiet('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
219
276
|
const currentBranch = branchRes.status === 0 ? branchRes.stdout.trim() : '';
|
|
@@ -692,15 +749,15 @@ function cmdUpdate(args = []) {
|
|
|
692
749
|
process.env.NEOAGENT_RELEASE_CHANNEL = releaseChannel;
|
|
693
750
|
logOk(`Release channel set to ${releaseChannelSummary(releaseChannel)}`);
|
|
694
751
|
}
|
|
695
|
-
const targetBranch = getReleaseChannelBranch(releaseChannel);
|
|
696
|
-
const npmTag = getReleaseChannelDistTag(releaseChannel);
|
|
697
752
|
const versionBefore = currentInstalledVersionLabel();
|
|
698
753
|
let versionAfter = versionBefore;
|
|
699
754
|
|
|
700
755
|
if (fs.existsSync(path.join(APP_DIR, '.git')) && commandExists('git')) {
|
|
701
756
|
const current = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
|
|
702
757
|
|
|
703
|
-
runOrThrow('git', ['fetch', 'origin',
|
|
758
|
+
runOrThrow('git', ['fetch', 'origin', '--tags']);
|
|
759
|
+
const targetBranch = resolvePreferredGitBranch(releaseChannel);
|
|
760
|
+
logInfo(`Using git branch ${targetBranch} for the ${releaseChannel} channel.`);
|
|
704
761
|
ensureGitBranchForReleaseChannel(targetBranch);
|
|
705
762
|
runOrThrow('git', ['pull', '--rebase', '--autostash', 'origin', targetBranch]);
|
|
706
763
|
|
|
@@ -714,6 +771,7 @@ function cmdUpdate(args = []) {
|
|
|
714
771
|
buildBundledWebClientIfPossible();
|
|
715
772
|
}
|
|
716
773
|
} else {
|
|
774
|
+
const npmTag = resolvePreferredNpmTag(releaseChannel);
|
|
717
775
|
logWarn(`No git repo detected; attempting npm global update from ${npmTag}.`);
|
|
718
776
|
if (commandExists('npm')) {
|
|
719
777
|
try {
|
package/package.json
CHANGED
|
@@ -14,6 +14,14 @@ const RELEASE_CHANNEL_DIST_TAGS = Object.freeze({
|
|
|
14
14
|
stable: 'latest',
|
|
15
15
|
beta: 'beta',
|
|
16
16
|
});
|
|
17
|
+
const RELEASE_CHANNEL_BRANCH_POLICIES = Object.freeze({
|
|
18
|
+
stable: 'main only',
|
|
19
|
+
beta: 'newest of beta or main',
|
|
20
|
+
});
|
|
21
|
+
const RELEASE_CHANNEL_NPM_POLICIES = Object.freeze({
|
|
22
|
+
stable: 'latest only',
|
|
23
|
+
beta: 'newest of beta or latest',
|
|
24
|
+
});
|
|
17
25
|
|
|
18
26
|
function parseEnv(raw) {
|
|
19
27
|
const map = new Map();
|
|
@@ -62,6 +70,116 @@ function getReleaseChannelLabel(channel) {
|
|
|
62
70
|
return normalizeReleaseChannel(channel) === 'beta' ? 'Beta' : 'Stable';
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
function parseSemver(version) {
|
|
74
|
+
const match = String(version || '')
|
|
75
|
+
.trim()
|
|
76
|
+
.replace(/^v/, '')
|
|
77
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
major: Number(match[1]),
|
|
84
|
+
minor: Number(match[2]),
|
|
85
|
+
patch: Number(match[3]),
|
|
86
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
87
|
+
raw: match[0],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function comparePrereleasePart(left, right) {
|
|
92
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
93
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
94
|
+
|
|
95
|
+
if (leftNumeric && rightNumeric) {
|
|
96
|
+
return Number(left) - Number(right);
|
|
97
|
+
}
|
|
98
|
+
if (leftNumeric) return -1;
|
|
99
|
+
if (rightNumeric) return 1;
|
|
100
|
+
return left.localeCompare(right);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compareVersions(leftVersion, rightVersion) {
|
|
104
|
+
const left = parseSemver(leftVersion);
|
|
105
|
+
const right = parseSemver(rightVersion);
|
|
106
|
+
|
|
107
|
+
if (!left && !right) return 0;
|
|
108
|
+
if (!left) return -1;
|
|
109
|
+
if (!right) return 1;
|
|
110
|
+
|
|
111
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
112
|
+
if (left[key] !== right[key]) {
|
|
113
|
+
return left[key] - right[key];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const leftPre = left.prerelease;
|
|
118
|
+
const rightPre = right.prerelease;
|
|
119
|
+
if (leftPre.length === 0 && rightPre.length === 0) return 0;
|
|
120
|
+
if (leftPre.length === 0) return 1;
|
|
121
|
+
if (rightPre.length === 0) return -1;
|
|
122
|
+
|
|
123
|
+
const length = Math.max(leftPre.length, rightPre.length);
|
|
124
|
+
for (let i = 0; i < length; i++) {
|
|
125
|
+
const leftPart = leftPre[i];
|
|
126
|
+
const rightPart = rightPre[i];
|
|
127
|
+
if (leftPart == null) return -1;
|
|
128
|
+
if (rightPart == null) return 1;
|
|
129
|
+
const diff = comparePrereleasePart(leftPart, rightPart);
|
|
130
|
+
if (diff !== 0) {
|
|
131
|
+
return diff;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function maxVersion(leftVersion, rightVersion) {
|
|
139
|
+
return compareVersions(leftVersion, rightVersion) >= 0 ? leftVersion : rightVersion;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function describeReleaseChannelPolicy(channel) {
|
|
143
|
+
const normalized = normalizeReleaseChannel(channel);
|
|
144
|
+
return `${getReleaseChannelLabel(normalized)} (git ${RELEASE_CHANNEL_BRANCH_POLICIES[normalized]}, npm ${RELEASE_CHANNEL_NPM_POLICIES[normalized]})`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getReleaseChannelBranchPolicy(channel) {
|
|
148
|
+
return RELEASE_CHANNEL_BRANCH_POLICIES[normalizeReleaseChannel(channel)];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getReleaseChannelNpmPolicy(channel) {
|
|
152
|
+
return RELEASE_CHANNEL_NPM_POLICIES[normalizeReleaseChannel(channel)];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function choosePreferredBranchForChannel(channel, versions = {}) {
|
|
156
|
+
const normalized = normalizeReleaseChannel(channel);
|
|
157
|
+
if (normalized === 'stable') {
|
|
158
|
+
return 'main';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const stableVersion = versions.stable;
|
|
162
|
+
const betaVersion = versions.beta;
|
|
163
|
+
if (compareVersions(betaVersion, stableVersion) > 0) {
|
|
164
|
+
return 'beta';
|
|
165
|
+
}
|
|
166
|
+
return 'main';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function choosePreferredNpmTagForChannel(channel, versions = {}) {
|
|
170
|
+
const normalized = normalizeReleaseChannel(channel);
|
|
171
|
+
if (normalized === 'stable') {
|
|
172
|
+
return 'latest';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const stableVersion = versions.latest;
|
|
176
|
+
const betaVersion = versions.beta;
|
|
177
|
+
if (compareVersions(betaVersion, stableVersion) > 0) {
|
|
178
|
+
return 'beta';
|
|
179
|
+
}
|
|
180
|
+
return 'latest';
|
|
181
|
+
}
|
|
182
|
+
|
|
65
183
|
function readReleaseChannelFromRaw(raw) {
|
|
66
184
|
const env = parseEnv(raw);
|
|
67
185
|
return normalizeReleaseChannel(env.get(RELEASE_CHANNEL_ENV_KEY));
|
|
@@ -116,6 +234,14 @@ module.exports = {
|
|
|
116
234
|
getReleaseChannelBranch,
|
|
117
235
|
getReleaseChannelDistTag,
|
|
118
236
|
getReleaseChannelLabel,
|
|
237
|
+
parseSemver,
|
|
238
|
+
compareVersions,
|
|
239
|
+
maxVersion,
|
|
240
|
+
describeReleaseChannelPolicy,
|
|
241
|
+
getReleaseChannelBranchPolicy,
|
|
242
|
+
getReleaseChannelNpmPolicy,
|
|
243
|
+
choosePreferredBranchForChannel,
|
|
244
|
+
choosePreferredNpmTagForChannel,
|
|
119
245
|
readReleaseChannelFromRaw,
|
|
120
246
|
readReleaseChannelFromEnvFile,
|
|
121
247
|
readConfiguredReleaseChannel,
|
package/server/db/database.js
CHANGED
|
@@ -40,6 +40,7 @@ db.exec(`
|
|
|
40
40
|
total_tokens INTEGER DEFAULT 0,
|
|
41
41
|
prompt_metrics TEXT,
|
|
42
42
|
error TEXT,
|
|
43
|
+
final_response TEXT,
|
|
43
44
|
created_at TEXT DEFAULT (datetime('now')),
|
|
44
45
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
45
46
|
completed_at TEXT,
|
|
@@ -389,6 +390,7 @@ for (const col of [
|
|
|
389
390
|
"ALTER TABLE scheduled_tasks ADD COLUMN run_at TEXT",
|
|
390
391
|
"ALTER TABLE scheduled_tasks ADD COLUMN one_time INTEGER DEFAULT 0",
|
|
391
392
|
"ALTER TABLE agent_runs ADD COLUMN prompt_metrics TEXT",
|
|
393
|
+
"ALTER TABLE agent_runs ADD COLUMN final_response TEXT",
|
|
392
394
|
"ALTER TABLE conversations ADD COLUMN summary TEXT",
|
|
393
395
|
"ALTER TABLE conversations ADD COLUMN summary_message_count INTEGER DEFAULT 0",
|
|
394
396
|
"ALTER TABLE conversations ADD COLUMN last_summary TEXT",
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"052f31d115eceda8cbff1b3481fcde4330c4ae
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "4059413374" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
package/server/routes/agents.js
CHANGED
|
@@ -94,11 +94,23 @@ router.get('/:id/steps', (req, res) => {
|
|
|
94
94
|
if (!run) return res.status(404).json({ error: 'Run not found' });
|
|
95
95
|
|
|
96
96
|
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(run.id);
|
|
97
|
-
const
|
|
97
|
+
const historyResponse = db.prepare(
|
|
98
98
|
`SELECT content FROM conversation_history WHERE user_id = ? AND agent_run_id = ? AND role = 'assistant' ORDER BY created_at DESC LIMIT 1`
|
|
99
99
|
).get(req.session.userId, run.id);
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const sentMessages = db.prepare(
|
|
101
|
+
`SELECT content FROM messages WHERE user_id = ? AND run_id = ? AND role = 'assistant' ORDER BY created_at ASC, id ASC`
|
|
102
|
+
).all(req.session.userId, run.id);
|
|
103
|
+
const sentResponse = sentMessages
|
|
104
|
+
.map((row) => row?.content?.toString().trim() || '')
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join('\n\n');
|
|
107
|
+
const response =
|
|
108
|
+
sentResponse
|
|
109
|
+
|| historyResponse?.content
|
|
110
|
+
|| run.final_response
|
|
111
|
+
|| null;
|
|
112
|
+
|
|
113
|
+
res.json({ run, steps, response });
|
|
102
114
|
});
|
|
103
115
|
|
|
104
116
|
// Abort a run
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
2
|
const router = express.Router();
|
|
5
3
|
const db = require('../db/database');
|
|
6
4
|
const { requireAuth } = require('../middleware/auth');
|
|
7
5
|
const { normalizeWhatsAppWhitelist } = require('../utils/whatsapp');
|
|
8
6
|
const { getVersionInfo } = require('../utils/version');
|
|
9
|
-
const {
|
|
7
|
+
const { APP_DIR } = require('../../runtime/paths');
|
|
8
|
+
const {
|
|
9
|
+
readUpdateStatus,
|
|
10
|
+
writeUpdateStatusFile: writeUpdateStatus,
|
|
11
|
+
} = require('../utils/update_status');
|
|
10
12
|
const {
|
|
11
13
|
parseReleaseChannel,
|
|
12
|
-
getReleaseChannelBranch,
|
|
13
|
-
getReleaseChannelDistTag,
|
|
14
14
|
writeReleaseChannelToEnvFile,
|
|
15
|
+
getReleaseChannelBranchPolicy,
|
|
16
|
+
getReleaseChannelNpmPolicy,
|
|
15
17
|
} = require('../../runtime/release_channel');
|
|
16
18
|
const {
|
|
17
19
|
createDefaultAiSettings,
|
|
@@ -21,36 +23,6 @@ const {
|
|
|
21
23
|
|
|
22
24
|
router.use(requireAuth);
|
|
23
25
|
|
|
24
|
-
function readUpdateStatus() {
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(fs.readFileSync(UPDATE_STATUS_FILE, 'utf8'));
|
|
27
|
-
} catch {
|
|
28
|
-
return {
|
|
29
|
-
state: 'idle',
|
|
30
|
-
progress: 0,
|
|
31
|
-
phase: 'idle',
|
|
32
|
-
message: 'No update running',
|
|
33
|
-
startedAt: null,
|
|
34
|
-
completedAt: null,
|
|
35
|
-
versionBefore: null,
|
|
36
|
-
versionAfter: null,
|
|
37
|
-
changelog: [],
|
|
38
|
-
logs: []
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function writeUpdateStatus(patch) {
|
|
44
|
-
const next = {
|
|
45
|
-
...readUpdateStatus(),
|
|
46
|
-
...patch,
|
|
47
|
-
updatedAt: new Date().toISOString()
|
|
48
|
-
};
|
|
49
|
-
fs.mkdirSync(path.dirname(UPDATE_STATUS_FILE), { recursive: true });
|
|
50
|
-
fs.writeFileSync(UPDATE_STATUS_FILE, JSON.stringify(next, null, 2));
|
|
51
|
-
return next;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
26
|
function canApplyGlobalBrowserSetting(userId) {
|
|
55
27
|
const users = db.prepare('SELECT id FROM users ORDER BY id ASC').all();
|
|
56
28
|
return users.length <= 1 || users[0]?.id === userId;
|
|
@@ -288,6 +260,12 @@ router.post('/update', (req, res) => {
|
|
|
288
260
|
return res.status(409).json({ success: false, error: 'An update is already running' });
|
|
289
261
|
}
|
|
290
262
|
console.log('[Settings] Triggering update-runner...');
|
|
263
|
+
const child = spawn(process.execPath, ['scripts/update-runner.js'], {
|
|
264
|
+
detached: true,
|
|
265
|
+
stdio: 'ignore',
|
|
266
|
+
cwd: APP_DIR
|
|
267
|
+
});
|
|
268
|
+
|
|
291
269
|
writeUpdateStatus({
|
|
292
270
|
state: 'running',
|
|
293
271
|
progress: 1,
|
|
@@ -297,24 +275,19 @@ router.post('/update', (req, res) => {
|
|
|
297
275
|
completedAt: null,
|
|
298
276
|
versionBefore: null,
|
|
299
277
|
versionAfter: null,
|
|
278
|
+
runnerPid: child.pid,
|
|
300
279
|
changelog: [],
|
|
301
280
|
logs: []
|
|
302
281
|
});
|
|
303
282
|
|
|
304
|
-
// Spawn detached runner so status survives server restarts.
|
|
305
|
-
const child = spawn(process.execPath, ['scripts/update-runner.js'], {
|
|
306
|
-
detached: true,
|
|
307
|
-
stdio: 'ignore',
|
|
308
|
-
cwd: APP_DIR
|
|
309
|
-
});
|
|
310
|
-
|
|
311
283
|
child.once('error', (error) => {
|
|
312
284
|
writeUpdateStatus({
|
|
313
285
|
state: 'failed',
|
|
314
286
|
progress: 100,
|
|
315
287
|
phase: 'failed',
|
|
316
288
|
message: `Failed to launch update job: ${error.message}`,
|
|
317
|
-
completedAt: new Date().toISOString()
|
|
289
|
+
completedAt: new Date().toISOString(),
|
|
290
|
+
runnerPid: null,
|
|
318
291
|
});
|
|
319
292
|
});
|
|
320
293
|
|
|
@@ -338,8 +311,8 @@ router.put('/update/channel', (req, res) => {
|
|
|
338
311
|
res.json({
|
|
339
312
|
success: true,
|
|
340
313
|
releaseChannel,
|
|
341
|
-
targetBranch:
|
|
342
|
-
npmDistTag:
|
|
314
|
+
targetBranch: getReleaseChannelBranchPolicy(releaseChannel),
|
|
315
|
+
npmDistTag: getReleaseChannelNpmPolicy(releaseChannel),
|
|
343
316
|
});
|
|
344
317
|
});
|
|
345
318
|
|
|
@@ -354,9 +327,9 @@ router.get('/update/status', (req, res) => {
|
|
|
354
327
|
gitVersion: version.gitVersion,
|
|
355
328
|
gitSha: version.gitSha,
|
|
356
329
|
gitBranch: version.gitBranch,
|
|
357
|
-
releaseChannel: version.releaseChannel,
|
|
358
|
-
targetBranch: version.targetBranch,
|
|
359
|
-
npmDistTag: version.npmDistTag,
|
|
330
|
+
releaseChannel: status.releaseChannel || version.releaseChannel,
|
|
331
|
+
targetBranch: status.targetBranch || version.targetBranch,
|
|
332
|
+
npmDistTag: status.npmDistTag || version.npmDistTag,
|
|
360
333
|
});
|
|
361
334
|
});
|
|
362
335
|
|
|
@@ -138,6 +138,22 @@ function normalizeOutgoingMessage(content) {
|
|
|
138
138
|
.trim();
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function joinSentMessages(messages = []) {
|
|
142
|
+
if (!Array.isArray(messages)) return '';
|
|
143
|
+
return messages
|
|
144
|
+
.map((message) => String(message || '').trim())
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join('\n\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildForcedFinalReplyPrompt(triggerSource) {
|
|
150
|
+
if (triggerSource === 'messaging') {
|
|
151
|
+
return 'Tool work is finished. Write the user-visible reply that should be sent back now. Do not call tools. Do not use [NO RESPONSE] unless the user explicitly asked for silence or no confirmation.';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return 'Tool work is finished. Write the final user-facing reply now. Do not call tools.';
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
function clampRunContext(text, maxChars) {
|
|
142
158
|
const value = normalizeOutgoingMessage(text);
|
|
143
159
|
if (!value) return '';
|
|
@@ -458,6 +474,7 @@ class AgentEngine {
|
|
|
458
474
|
aborted: false,
|
|
459
475
|
messagingSent: false,
|
|
460
476
|
lastSentMessage: '',
|
|
477
|
+
sentMessages: [],
|
|
461
478
|
triggerType,
|
|
462
479
|
triggerSource,
|
|
463
480
|
startedAt: Date.now(),
|
|
@@ -506,7 +523,6 @@ class AgentEngine {
|
|
|
506
523
|
let totalTokens = 0;
|
|
507
524
|
let lastContent = '';
|
|
508
525
|
let stepIndex = 0;
|
|
509
|
-
let forcedFinalResponse = false;
|
|
510
526
|
let promptMetrics = {};
|
|
511
527
|
|
|
512
528
|
try {
|
|
@@ -766,12 +782,17 @@ class AgentEngine {
|
|
|
766
782
|
|
|
767
783
|
if ((iteration >= maxIterations && messages[messages.length - 1]?.role === 'tool')
|
|
768
784
|
|| (iteration < maxIterations && stepIndex > 0 && !lastContent.trim() && messages[messages.length - 1]?.role !== 'tool')) {
|
|
769
|
-
const finalResponse = await provider.chat(sanitizeConversationMessages(
|
|
785
|
+
const finalResponse = await provider.chat(sanitizeConversationMessages([
|
|
786
|
+
...messages,
|
|
787
|
+
{
|
|
788
|
+
role: 'system',
|
|
789
|
+
content: buildForcedFinalReplyPrompt(triggerSource)
|
|
790
|
+
}
|
|
791
|
+
]), [], {
|
|
770
792
|
model,
|
|
771
793
|
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
772
794
|
});
|
|
773
795
|
lastContent = sanitizeModelOutput(finalResponse.content || '', { model });
|
|
774
|
-
forcedFinalResponse = true;
|
|
775
796
|
|
|
776
797
|
const finalAssistantMessage = { role: 'assistant', content: lastContent };
|
|
777
798
|
if (finalResponse.providerContentBlocks?.length) {
|
|
@@ -785,8 +806,18 @@ class AgentEngine {
|
|
|
785
806
|
totalTokens += finalResponse.usage?.totalTokens || 0;
|
|
786
807
|
}
|
|
787
808
|
|
|
788
|
-
|
|
789
|
-
|
|
809
|
+
const runMeta = this.activeRuns.get(runId);
|
|
810
|
+
const messagingSent = runMeta?.messagingSent || false;
|
|
811
|
+
const sentMessageText = joinSentMessages(runMeta?.sentMessages);
|
|
812
|
+
const finalResponseText = lastContent.trim() ? lastContent : sentMessageText;
|
|
813
|
+
const lastSentMessage = normalizeOutgoingMessage(
|
|
814
|
+
runMeta?.lastSentMessage
|
|
815
|
+
|| (Array.isArray(runMeta?.sentMessages) ? runMeta.sentMessages[runMeta.sentMessages.length - 1] : '')
|
|
816
|
+
|| ''
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
db.prepare('UPDATE agent_runs SET status = ?, total_tokens = ?, final_response = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
|
|
820
|
+
.run('completed', totalTokens, finalResponseText || null, runId);
|
|
790
821
|
|
|
791
822
|
if (conversationId) {
|
|
792
823
|
db.prepare('UPDATE conversations SET total_tokens = total_tokens + ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
@@ -805,13 +836,10 @@ class AgentEngine {
|
|
|
805
836
|
triggerSource,
|
|
806
837
|
runTitle,
|
|
807
838
|
userMessage,
|
|
808
|
-
lastContent,
|
|
839
|
+
lastContent: finalResponseText,
|
|
809
840
|
stepIndex
|
|
810
841
|
});
|
|
811
842
|
|
|
812
|
-
const runMeta = this.activeRuns.get(runId);
|
|
813
|
-
const messagingSent = runMeta?.messagingSent || false;
|
|
814
|
-
const lastSentMessage = normalizeOutgoingMessage(runMeta?.lastSentMessage || '');
|
|
815
843
|
this.activeRuns.delete(runId);
|
|
816
844
|
this.emit(userId, 'run:complete', { runId, content: lastContent, totalTokens, iterations: iteration, triggerSource });
|
|
817
845
|
|
|
@@ -836,7 +864,7 @@ class AgentEngine {
|
|
|
836
864
|
await manager.sendTyping(userId, options.source, options.chatId, true).catch(() => { });
|
|
837
865
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
838
866
|
}
|
|
839
|
-
await manager.sendMessage(userId, options.source, options.chatId, chunks[i]).catch((err) =>
|
|
867
|
+
await manager.sendMessage(userId, options.source, options.chatId, chunks[i], { runId }).catch((err) =>
|
|
840
868
|
console.error('[Engine] Auto-reply fallback failed:', err.message)
|
|
841
869
|
);
|
|
842
870
|
}
|
|
@@ -457,7 +457,7 @@ function getAvailableTools(app, options = {}) {
|
|
|
457
457
|
},
|
|
458
458
|
{
|
|
459
459
|
name: 'send_message',
|
|
460
|
-
description: 'Send a message on a connected messaging platform. Supports WhatsApp (text/media), Telnyx Voice (phone calls — TTS), Discord, and Telegram. For WhatsApp: use media_path to attach files.
|
|
460
|
+
description: 'Send a message on a connected messaging platform. Supports WhatsApp (text/media), Telnyx Voice (phone calls — TTS), Discord, and Telegram. For WhatsApp: use media_path to attach files. Use content "[NO RESPONSE]" only when the user explicitly asked for silence or no reply. For Telnyx Voice: always reply with plain spoken text; never use [NO RESPONSE] or markdown.',
|
|
461
461
|
parameters: {
|
|
462
462
|
type: 'object',
|
|
463
463
|
properties: {
|
|
@@ -1113,12 +1113,18 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1113
1113
|
case 'send_message': {
|
|
1114
1114
|
const manager = msg();
|
|
1115
1115
|
if (!manager) return { error: 'Messaging not available' };
|
|
1116
|
-
const sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content,
|
|
1116
|
+
const sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content, {
|
|
1117
|
+
mediaPath: args.media_path,
|
|
1118
|
+
runId
|
|
1119
|
+
});
|
|
1117
1120
|
// Track that the agent explicitly sent a message during this run
|
|
1118
1121
|
const runState = runId ? engine.activeRuns.get(runId) : null;
|
|
1119
1122
|
if (runState && args.content !== '[NO RESPONSE]') {
|
|
1120
1123
|
runState.messagingSent = true;
|
|
1121
1124
|
runState.lastSentMessage = args.content || '';
|
|
1125
|
+
if (Array.isArray(runState.sentMessages)) {
|
|
1126
|
+
runState.sentMessages.push(args.content || '');
|
|
1127
|
+
}
|
|
1122
1128
|
}
|
|
1123
1129
|
return sendResult;
|
|
1124
1130
|
}
|
|
@@ -1369,7 +1375,9 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1369
1375
|
}
|
|
1370
1376
|
|
|
1371
1377
|
try {
|
|
1372
|
-
const sendResult = await manager.sendMessage(userId, target.platform, target.to, message
|
|
1378
|
+
const sendResult = await manager.sendMessage(userId, target.platform, target.to, message, {
|
|
1379
|
+
runId
|
|
1380
|
+
});
|
|
1373
1381
|
if (taskId && taskConfig && (taskConfig.notifyPlatform !== target.platform || taskConfig.notifyTo !== target.to)) {
|
|
1374
1382
|
taskConfig.notifyPlatform = target.platform;
|
|
1375
1383
|
taskConfig.notifyTo = target.to;
|
|
@@ -1381,6 +1389,9 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1381
1389
|
if (runState) {
|
|
1382
1390
|
runState.messagingSent = true;
|
|
1383
1391
|
runState.lastSentMessage = message;
|
|
1392
|
+
if (Array.isArray(runState.sentMessages)) {
|
|
1393
|
+
runState.sentMessages.push(message);
|
|
1394
|
+
}
|
|
1384
1395
|
}
|
|
1385
1396
|
return {
|
|
1386
1397
|
sent: true,
|
|
@@ -200,7 +200,7 @@ function buildIncomingPrompt(msg) {
|
|
|
200
200
|
return `You are on a live phone call. The caller (${msg.senderName || msg.sender}) said:\n<caller_speech>\n${msg.content}\n</caller_speech>\n\nRespond via send_message with platform="telnyx" and to="${msg.chatId}".`;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
return `You received a ${msg.platform} message from ${msg.senderName || msg.sender} (chat: ${msg.chatId}):\n<external_message>\n${msg.content}\n</external_message>${mediaNote}${discordContext}${sttNote}\n\nReply via send_message with platform="${msg.platform}" and to="${msg.chatId}".`;
|
|
203
|
+
return `You received a ${msg.platform} message from ${msg.senderName || msg.sender} (chat: ${msg.chatId}):\n<external_message>\n${msg.content}\n</external_message>${mediaNote}${discordContext}${sttNote}\n\nReply via send_message with platform="${msg.platform}" and to="${msg.chatId}". Send at least one user-visible reply before you finish. Do not use [NO RESPONSE] unless the user explicitly asked for silence or no confirmation.`;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
async function isAllowedMessagingSender({ io, userId, msg }) {
|
|
@@ -164,11 +164,18 @@ class MessagingManager {
|
|
|
164
164
|
return { status: 'disconnected' };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
async sendMessage(userId, platformName, to, content,
|
|
167
|
+
async sendMessage(userId, platformName, to, content, mediaPathOrOptions) {
|
|
168
168
|
const key = `${userId}:${platformName}`;
|
|
169
169
|
const platform = this.platforms.get(key);
|
|
170
170
|
if (!platform) throw new Error(`Platform ${platformName} not connected`);
|
|
171
171
|
|
|
172
|
+
const sendOptions =
|
|
173
|
+
mediaPathOrOptions && typeof mediaPathOrOptions === 'object' && !Array.isArray(mediaPathOrOptions)
|
|
174
|
+
? mediaPathOrOptions
|
|
175
|
+
: { mediaPath: mediaPathOrOptions };
|
|
176
|
+
const mediaPath = sendOptions.mediaPath || null;
|
|
177
|
+
const runId = sendOptions.runId || null;
|
|
178
|
+
|
|
172
179
|
// Sentinel: agent can choose not to reply by sending [NO RESPONSE]
|
|
173
180
|
if (!mediaPath && typeof content === 'string' && content.trim().toUpperCase() === '[NO RESPONSE]') {
|
|
174
181
|
return { success: true, suppressed: true };
|
|
@@ -176,15 +183,16 @@ class MessagingManager {
|
|
|
176
183
|
|
|
177
184
|
const result = await platform.sendMessage(to, content, { mediaPath });
|
|
178
185
|
|
|
179
|
-
db.prepare('INSERT INTO messages (user_id, role, content, platform, platform_chat_id, media_path) VALUES (?, ?, ?, ?, ?, ?)')
|
|
180
|
-
.run(userId, 'assistant', content, platformName, to, mediaPath
|
|
186
|
+
db.prepare('INSERT INTO messages (user_id, run_id, role, content, platform, platform_chat_id, media_path) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
187
|
+
.run(userId, runId, 'assistant', content, platformName, to, mediaPath);
|
|
181
188
|
|
|
182
189
|
// Notify the web UI so the sent message appears in chat
|
|
183
190
|
this.io.to(`user:${userId}`).emit('messaging:sent', {
|
|
184
191
|
platform: platformName,
|
|
185
192
|
to,
|
|
186
193
|
content,
|
|
187
|
-
mediaPath
|
|
194
|
+
mediaPath,
|
|
195
|
+
runId
|
|
188
196
|
});
|
|
189
197
|
|
|
190
198
|
return { success: true, result };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { UPDATE_STATUS_FILE } = require('../../runtime/paths');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_UPDATE_STATUS = Object.freeze({
|
|
8
|
+
state: 'idle',
|
|
9
|
+
progress: 0,
|
|
10
|
+
phase: 'idle',
|
|
11
|
+
message: 'No update running',
|
|
12
|
+
startedAt: null,
|
|
13
|
+
completedAt: null,
|
|
14
|
+
versionBefore: null,
|
|
15
|
+
versionAfter: null,
|
|
16
|
+
runnerPid: null,
|
|
17
|
+
changelog: [],
|
|
18
|
+
logs: [],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function isProcessAlive(pid) {
|
|
22
|
+
const numericPid = Number(pid);
|
|
23
|
+
if (!Number.isInteger(numericPid) || numericPid <= 0) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
process.kill(numericPid, 0);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readUpdateStatusFile(filePath = UPDATE_STATUS_FILE) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return { ...DEFAULT_UPDATE_STATUS };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeUpdateStatus(status) {
|
|
44
|
+
const next = {
|
|
45
|
+
...DEFAULT_UPDATE_STATUS,
|
|
46
|
+
...(status || {}),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (next.state === 'running' && !isProcessAlive(next.runnerPid)) {
|
|
50
|
+
return {
|
|
51
|
+
...next,
|
|
52
|
+
state: 'failed',
|
|
53
|
+
progress: 100,
|
|
54
|
+
phase: 'failed',
|
|
55
|
+
message: 'Previous update job stopped unexpectedly. You can try the update again.',
|
|
56
|
+
completedAt: next.completedAt || new Date().toISOString(),
|
|
57
|
+
runnerPid: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeUpdateStatusFile(patch, filePath = UPDATE_STATUS_FILE) {
|
|
65
|
+
const current = normalizeUpdateStatus(readUpdateStatusFile(filePath));
|
|
66
|
+
const next = normalizeUpdateStatus({
|
|
67
|
+
...current,
|
|
68
|
+
...patch,
|
|
69
|
+
updatedAt: new Date().toISOString(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
73
|
+
fs.writeFileSync(filePath, JSON.stringify(next, null, 2));
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readUpdateStatus(filePath = UPDATE_STATUS_FILE) {
|
|
78
|
+
const raw = readUpdateStatusFile(filePath);
|
|
79
|
+
const normalized = normalizeUpdateStatus(raw);
|
|
80
|
+
if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
81
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
82
|
+
fs.writeFileSync(filePath, JSON.stringify(normalized, null, 2));
|
|
83
|
+
}
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
DEFAULT_UPDATE_STATUS,
|
|
89
|
+
isProcessAlive,
|
|
90
|
+
normalizeUpdateStatus,
|
|
91
|
+
readUpdateStatus,
|
|
92
|
+
writeUpdateStatusFile,
|
|
93
|
+
};
|
package/server/utils/version.js
CHANGED
|
@@ -5,9 +5,9 @@ const path = require('path');
|
|
|
5
5
|
const { execSync } = require('child_process');
|
|
6
6
|
const { APP_DIR } = require('../../runtime/paths');
|
|
7
7
|
const {
|
|
8
|
-
getReleaseChannelBranch,
|
|
9
|
-
getReleaseChannelDistTag,
|
|
10
8
|
readConfiguredReleaseChannel,
|
|
9
|
+
getReleaseChannelBranchPolicy,
|
|
10
|
+
getReleaseChannelNpmPolicy,
|
|
11
11
|
} = require('../../runtime/release_channel');
|
|
12
12
|
|
|
13
13
|
const PACKAGE_JSON_PATH = path.join(APP_DIR, 'package.json');
|
|
@@ -67,8 +67,8 @@ function getVersionInfo() {
|
|
|
67
67
|
gitSha,
|
|
68
68
|
installedVersion: packageVersion,
|
|
69
69
|
releaseChannel,
|
|
70
|
-
targetBranch:
|
|
71
|
-
npmDistTag:
|
|
70
|
+
targetBranch: getReleaseChannelBranchPolicy(releaseChannel),
|
|
71
|
+
npmDistTag: getReleaseChannelNpmPolicy(releaseChannel),
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|