agentlas 1.0.61 → 1.0.63

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.
@@ -183,6 +183,13 @@ class Session extends EventEmitter {
183
183
  this.lastError = null;
184
184
  this._privateRecoveryEvidence.length = 0;
185
185
  this._record({ type: "turn-start", at: Date.now(), prompt });
186
+ // ACP agents that cannot load a provider-side session need prior conversation
187
+ // reattached on a fresh session. Capture it before appending this turn so the
188
+ // current user prompt is not duplicated in both history and userPrompt.
189
+ const priorHistory = store.chatHistory(this.db, this.chatId).map((row) => ({
190
+ role: row.role,
191
+ text: row.text,
192
+ }));
186
193
  store.appendMessage(this.db, this.chatId, "user", prompt);
187
194
  let governedTurn = null;
188
195
  try {
@@ -260,6 +267,9 @@ class Session extends EventEmitter {
260
267
  this._apiAbort = null;
261
268
  }
262
269
  } else {
270
+ const isAcpRuntime = require("../runtimes/acp-driver.cjs").ACP_KINDS.has(this.runtime.kind);
271
+ const turnAbort = isAcpRuntime ? new AbortController() : null;
272
+ if (turnAbort) this._apiAbort = turnAbort;
263
273
  const req = {
264
274
  kind: this.runtime.kind,
265
275
  bin: this.runtime.bin,
@@ -269,6 +279,11 @@ class Session extends EventEmitter {
269
279
  systemPrompt,
270
280
  permission: this.permission,
271
281
  session: { ...this.runtimeSession },
282
+ history: priorHistory,
283
+ chatId: this.chatId,
284
+ agentId: this.agent.id,
285
+ locale: this.lang,
286
+ sessionFingerprintSeed: this.fingerprint,
272
287
  model: this.runtime.model,
273
288
  effort: this.runtime.effort,
274
289
  // 사용자가 이미 동의한 MCP 서버를 턴에 싣는다.
@@ -286,6 +301,7 @@ class Session extends EventEmitter {
286
301
  mcpServers: this._consentedMcpServers(),
287
302
  onSpawn: (child) => { this._child = child; },
288
303
  };
304
+ if (turnAbort) req.signal = turnAbort.signal;
289
305
  if (this._spawnImpl) req.spawn = this._spawnImpl;
290
306
  if (this._timeoutConfig) req.timeoutConfig = this._timeoutConfig;
291
307
  try {
@@ -325,6 +341,8 @@ class Session extends EventEmitter {
325
341
  }
326
342
  } catch (e) {
327
343
  res = { text: "", session: req.session, error: (e && e.message) || String(e) };
344
+ } finally {
345
+ if (turnAbort && this._apiAbort === turnAbort) this._apiAbort = null;
328
346
  }
329
347
  }
330
348
  this._child = null;
@@ -55,10 +55,15 @@ async function verifyBotToken(token, opts) {
55
55
  function tokenDir() {
56
56
  const dir = path.join(userDataDir(), "telegram");
57
57
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
58
+ if (process.platform !== "win32") fs.chmodSync(dir, 0o700);
58
59
  return dir;
59
60
  }
60
61
  function tokenFile(id) { return path.join(tokenDir(), `${id}.token`); }
61
- function saveToken(id, token) { fs.writeFileSync(tokenFile(id), token, { encoding: "utf8", mode: 0o600 }); }
62
+ function saveToken(id, token) {
63
+ const file = tokenFile(id);
64
+ fs.writeFileSync(file, token, { encoding: "utf8", mode: 0o600 });
65
+ if (process.platform !== "win32") fs.chmodSync(file, 0o600);
66
+ }
62
67
  function readToken(id) {
63
68
  try { return fs.readFileSync(tokenFile(id), "utf8").trim() || null; } catch { return null; }
64
69
  }
@@ -81,19 +86,31 @@ async function startConnection(db, targetKind, targetId, token, opts) {
81
86
  runWriteTransaction(db, () => {
82
87
  db.prepare(
83
88
  "INSERT INTO telegram_bindings (id, target_kind, target_id, bot_user_id, bot_username, bot_display_name, status, enabled, token_saved, token_fingerprint, created_at, updated_at) " +
84
- "VALUES (?,?,?,?,?,?,'waiting_for_chat',1,1,?,?,?)",
89
+ "VALUES (?,?,?,?,?,?,'waiting_for_chat',1,0,?,?,?)",
85
90
  ).run(id, targetKind, targetId, me.id, me.username || null, me.first_name || null, tokenFingerprint(token), now, now);
86
91
  });
87
- saveToken(id, token);
92
+ try {
93
+ saveToken(id, token);
94
+ runWriteTransaction(db, () => {
95
+ db.prepare("UPDATE telegram_bindings SET token_saved=1, updated_at=? WHERE id=?").run(new Date().toISOString(), id);
96
+ });
97
+ } catch (error) {
98
+ deleteToken(id);
99
+ try {
100
+ runWriteTransaction(db, () => db.prepare("DELETE FROM telegram_bindings WHERE id=?").run(id));
101
+ } catch { /* preserve the original storage failure */ }
102
+ throw error;
103
+ }
88
104
  // 남은 웹훅이 있으면 getUpdates가 막히므로 제거(있어도 무해).
89
105
  await telegramApi(token, "deleteWebhook", { drop_pending_updates: false }, opts).catch(() => null);
90
106
  return { id, botUsername: me.username || null };
91
107
  }
92
108
 
93
109
  /**
94
- * getUpdates 폴링으로 private 메시지의 chat을 이 바인딩에 귀속한다.
95
- * 보안: 데스크탑과 같은 규칙 미페어링·enabled·waiting_for_chat 신선 바인딩
96
- * (30분 이내)에만, private 채팅만. 반환: 페어링된 바인딩 또는 null(시간초과).
110
+ * getUpdates 폴링으로 `/start <bindingId>`를 보낸 private chat을 이 바인딩에 귀속한다.
111
+ * 보안: 이름을 발견한 제3자의 메시지가 로컬 에이전트를 탈취하지 못하도록 정확한
112
+ * 페어링 토큰을 요구한다. 최종 UPDATE도 미페어링·enabled·waiting 상태를 조건으로 삼아
113
+ * 두 Terminal 프로세스가 같은 바인딩을 동시에 덮어쓰지 못하게 한다.
97
114
  */
98
115
  async function pairByPolling(db, id, { timeoutMs = 120_000, opts, onWait } = {}) {
99
116
  const token = readToken(id);
@@ -110,6 +127,8 @@ async function pairByPolling(db, id, { timeoutMs = 120_000, opts, onWait } = {})
110
127
  if (typeof update.update_id === "number") offset = Math.max(offset, update.update_id);
111
128
  const message = update.message;
112
129
  if (!message || !message.chat || message.chat.type !== "private") continue;
130
+ const pairingToken = String(message.text || "").match(/^\/start(?:@\w+)?\s+(\S+)/i)?.[1] || "";
131
+ if (pairingToken !== id) continue;
113
132
  // 신선 미페어링 바인딩인지 재확인(경합 방지) 후 귀속.
114
133
  const row = getBinding(db, id);
115
134
  if (!row || row.telegram_chat_id || row.status !== "waiting_for_chat") continue;
@@ -117,11 +136,13 @@ async function pairByPolling(db, id, { timeoutMs = 120_000, opts, onWait } = {})
117
136
  if (!Number.isFinite(createdAt) || Date.now() - createdAt > 30 * 60 * 1000) throw new Error("pairing window expired (30 min) — reconnect");
118
137
  const now = new Date().toISOString();
119
138
  const title = message.chat.title || [message.chat.first_name, message.chat.last_name].filter(Boolean).join(" ") || message.chat.username || String(message.chat.id);
120
- runWriteTransaction(db, () => {
121
- db.prepare("UPDATE telegram_bindings SET telegram_chat_id=?, telegram_chat_title=?, status='chat_paired', last_update_id=?, updated_at=? WHERE id=?")
122
- .run(String(message.chat.id), title, offset, now, id);
139
+ const claimed = runWriteTransaction(db, () => {
140
+ return db.prepare(
141
+ "UPDATE telegram_bindings SET telegram_chat_id=?, telegram_chat_title=?, status='chat_paired', last_update_id=?, updated_at=? " +
142
+ "WHERE id=? AND telegram_chat_id IS NULL AND enabled=1 AND status='waiting_for_chat'",
143
+ ).run(String(message.chat.id), title, offset, now, id).changes === 1;
123
144
  });
124
- return getBinding(db, id);
145
+ if (claimed) return getBinding(db, id);
125
146
  }
126
147
  if (offset) {
127
148
  runWriteTransaction(db, () => {
@@ -11,6 +11,8 @@
11
11
  * - 모든 화면은 ctx.out 이 아니라 ui 를 직접 받아 pi 프레임 안에 그린다.
12
12
  */
13
13
 
14
+ const path = require("node:path");
15
+
14
16
  function table(ui, rows, opts = {}) {
15
17
  // rows: [[col, col, …]] — 첫 행이 헤더. 폭은 CJK 셀 폭으로 계산한다.
16
18
  const { visWidth, truncateWidth } = require("./width.cjs");
@@ -52,6 +54,28 @@ function rows(db, sql, args = []) {
52
54
  }
53
55
  const shortTs = (v) => (v ? String(v).replace("T", " ").slice(0, 16) : "");
54
56
 
57
+ const ATTENTION_RUN_WHERE = `
58
+ r.id = (
59
+ SELECT r2.id FROM automation_runs r2
60
+ WHERE r2.automation_id IS r.automation_id
61
+ ORDER BY COALESCE(r2.started_at,'') DESC, r2.rowid DESC LIMIT 1
62
+ )
63
+ AND (
64
+ r.status IN ('error','partial','blocked','needs_input')
65
+ OR r.outcome IN ('needs_input','blocked','rejected')
66
+ OR (r.status = 'running' AND julianday(r.last_activity_at) < julianday('now','-15 minutes'))
67
+ )`;
68
+
69
+ function pathContains(root, candidate) {
70
+ if (!root) return false;
71
+ try {
72
+ const relative = path.relative(path.resolve(String(root)), path.resolve(String(candidate)));
73
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
55
79
  /* ── /dashboard — 데스크탑 dashboard 의 관제 패널 집합 ── */
56
80
  function dashboard(ui, db, en) {
57
81
  const chip = (paint, s) => paint(` ${s} `);
@@ -66,11 +90,12 @@ function dashboard(ui, db, en) {
66
90
  ui.line(` ${chip(ui.c.inverse, `${en ? "agents" : "에이전트"} ${local}`)} ${chip(ui.c.dim, `builtin ${builtin}`)} ${chip(ui.c.inverse, `${en ? "firms" : "회사"} ${firms}`)} ${chip(ui.c.dim, `${en ? "bookmarks" : "북마크"} ${marks}`)} ${chip(ui.c.dim, `${en ? "borrowed" : "대여"} ${borrowed}`)}`);
67
91
 
68
92
  // ── 확인 필요 (D1 숨은 계약 2: 없으면 실행이 조용히 멈춘 채 정상처럼 보인다) ──
69
- const pending = count(db, "SELECT COUNT(*) n FROM automation_node_approvals WHERE decision NOT IN ('approved','rejected')");
93
+ const pending = count(db, `SELECT COUNT(*) n FROM automation_runs r WHERE ${ATTENTION_RUN_WHERE}`);
70
94
  const stalled = rows(db,
71
95
  `SELECT r.id, a.name, r.status, r.last_activity_at
72
96
  FROM automation_runs r LEFT JOIN automations a ON a.id = r.automation_id
73
- WHERE r.status NOT IN ('ok','error','cancelled') ORDER BY COALESCE(r.last_activity_at,'') DESC LIMIT 5`);
97
+ WHERE ${ATTENTION_RUN_WHERE}
98
+ ORDER BY COALESCE(r.last_activity_at,'') DESC LIMIT 5`);
74
99
  ui.line("");
75
100
  ui.line(ui.c.bold(en ? "Needs attention" : "확인 필요"));
76
101
  if (!pending && !stalled.length) {
@@ -242,10 +267,10 @@ function projects(ui, db, en) {
242
267
  if (list.length) {
243
268
  table(ui, [[en ? "project" : "프로젝트", en ? "source" : "소스", en ? "chats" : "채팅", en ? "tasks" : "작업", en ? "updated" : "수정"],
244
269
  ...list.map((p) => [
245
- (p.folder_path && cwd.startsWith(p.folder_path) ? "▸ " : " ") + (p.name || p.id),
270
+ (pathContains(p.folder_path, cwd) ? "▸ " : " ") + (p.name || p.id),
246
271
  p.source_type || "local", String(p.chats), String(p.tasks), shortTs(p.updated_at)])],
247
272
  { cap: [30, 10, 6, 6, 16] });
248
- const here = list.find((p) => p.folder_path && cwd.startsWith(p.folder_path));
273
+ const here = list.find((p) => pathContains(p.folder_path, cwd));
249
274
  ui.line("");
250
275
  ui.line(here
251
276
  ? ui.c.dim(en ? `▸ this folder is connected to "${here.name}"` : `▸ 이 폴더는 "${here.name}"에 연결돼 있습니다`)
@@ -350,5 +375,4 @@ function firms(ui, db, en, ctx, arg) {
350
375
  void ctx;
351
376
  }
352
377
 
353
- module.exports = { dashboard, library, marketplace, settings, projects, automations, firms, table };
354
-
378
+ module.exports = { dashboard, library, marketplace, settings, projects, automations, firms, table, pathContains, ATTENTION_RUN_WHERE };
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "14",
3
- "url": "https://github.com/agentlas-ai/agentlas-terminal/releases/download/desktop-core-v14/desktop-core.tar.gz",
4
- "sha256": "483f618756018dfb82e8851152eb50f45ed691d35e434bbbb09d3b13d844b6b9",
5
- "sizeBytes": 18764550,
6
- "writtenAt": "2026-08-26T12:42:18.915Z"
2
+ "version": "15",
3
+ "url": "https://github.com/agentlas-ai/agentlas-terminal/releases/download/desktop-core-v15/desktop-core.tar.gz",
4
+ "sha256": "df39404b55ea4e6a3571e416167f64e75bcde92fdb86f19bd26e54746156f87b",
5
+ "sizeBytes": 18167726,
6
+ "writtenAt": "2026-08-29T08:38:53.468Z"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.61",
3
+ "version": "1.0.63",
4
4
  "description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"
@@ -27,6 +27,8 @@
27
27
  "files": [
28
28
  "bin/",
29
29
  "engine/",
30
+ "!engine/vendor/desktop-core/",
31
+ "!engine/vendor/desktop-core.tar.gz",
30
32
  "install.sh",
31
33
  "install.ps1",
32
34
  "CHANGELOG.md",