agentlas 1.0.12 → 1.0.15
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/CHANGELOG.md +53 -0
- package/README.md +4 -2
- package/bin/agentlas.cjs +17 -3
- package/engine/agentlas-cloud-runtime.cjs +8 -2
- package/engine/agentlas-config.cjs +25 -18
- package/engine/agentlas-core-harness.cjs +14 -1
- package/engine/agentlas-i18n.cjs +2 -0
- package/engine/agentlas-input.cjs +1 -1
- package/engine/agentlas-memory-governance.cjs +10 -0
- package/engine/agentlas-onboard.cjs +22 -5
- package/engine/agentlas-sqlite-policy.cjs +18 -7
- package/engine/agentlas-workforce.cjs +136 -19
- package/engine/agentlas-workload-routing.cjs +32 -4
- package/engine/agentlas.cjs +8 -0
- package/engine/automation/daemon.cjs +67 -30
- package/engine/automation/schedule.cjs +16 -0
- package/engine/automation/store.cjs +70 -12
- package/engine/bootstrap-schema.sql +788 -53
- package/engine/cloud-assets/cas.cjs +7 -2
- package/engine/cloud-assets/commands.cjs +2 -0
- package/engine/cloud-assets/package.cjs +49 -1
- package/engine/cloud-assets/state.cjs +35 -0
- package/engine/commands/automation.cjs +8 -0
- package/engine/commands/chats.cjs +6 -1
- package/engine/commands/firm.cjs +7 -0
- package/engine/commands/help.cjs +23 -2
- package/engine/commands/hep-cloud.cjs +31 -0
- package/engine/commands/hep-hub.cjs +30 -0
- package/engine/commands/hep-local.cjs +32 -0
- package/engine/commands/hep-network.cjs +43 -0
- package/engine/commands/index.cjs +38 -7
- package/engine/commands/open.cjs +5 -1
- package/engine/commands/run.cjs +12 -2
- package/engine/commands/setup.cjs +12 -11
- package/engine/commands/storm.cjs +5 -9
- package/engine/commands/swarm.cjs +4 -4
- package/engine/commands/uninstall.cjs +36 -2
- package/engine/commands/version.cjs +29 -0
- package/engine/commands/workforce.cjs +10 -10
- package/engine/core/schema-ensure.cjs +75 -0
- package/engine/experience/variant.cjs +46 -2
- package/engine/hephaestus/runtime.cjs +7 -0
- package/engine/memory-cli/curate.cjs +3 -7
- package/engine/project/memory-context.cjs +3 -7
- package/engine/project/state.cjs +7 -6
- package/engine/sessions/orchestrator.cjs +41 -3
- package/engine/sessions/prompt.cjs +4 -7
- package/engine/sessions/session.cjs +32 -0
- package/engine/storm/swarm.cjs +12 -0
- package/engine/ui/palette.cjs +14 -2
- package/engine/ui/renderer.cjs +37 -0
- package/engine/ui/repl.cjs +7 -1
- package/engine/workforce/concurrency.cjs +41 -0
- package/package.json +1 -1
|
@@ -11,7 +11,8 @@ const { runWriteTransaction } = require("../agentlas-sqlite-policy.cjs");
|
|
|
11
11
|
|
|
12
12
|
function run(ctx, args) {
|
|
13
13
|
const ko = ctx.lang === "ko";
|
|
14
|
-
|
|
14
|
+
// 동의 플래그를 슬러그보다 앞에 써도 되도록 첫 번째 비플래그 인자를 대상으로 본다.
|
|
15
|
+
const token = args.find((a) => !String(a).startsWith("-"));
|
|
15
16
|
if (!token) {
|
|
16
17
|
ctx.err(ko ? "사용법: agentlas uninstall <agent>" : "Usage: agentlas uninstall <agent>");
|
|
17
18
|
return 1;
|
|
@@ -45,6 +46,35 @@ function run(ctx, args) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/*
|
|
50
|
+
* 대화 파괴 게이트 — bootstrap-schema.sql:50 chats.agent_id 는
|
|
51
|
+
* `ON DELETE CASCADE`, :61 chat_messages.chat_id 도 CASCADE 다. 즉
|
|
52
|
+
* installed_agents 행 하나를 지우면 그 에이전트의 모든 챗과 메시지가 같이
|
|
53
|
+
* 사라진다(데스크탑과 공유하는 SQLite라 데스크탑 쪽 이력도 함께 증발한다).
|
|
54
|
+
* 그런데 이 명령은 그 사실을 한 줄도 알리지 않고 `✓ 제거됨`만 찍었다 —
|
|
55
|
+
* 되돌릴 수 없는 삭제가 무고지·무확인으로 일어나던 자리다.
|
|
56
|
+
* 그래서 지우기 "전에" 파급 건수를 세고, 대화가 있으면 명시적 동의
|
|
57
|
+
* (--yes/-y/--force) 없이는 아무것도 지우지 않는다. 대화가 0건이면
|
|
58
|
+
* 파괴할 이력이 없으므로 종전대로 그냥 진행한다(불필요한 마찰 금지).
|
|
59
|
+
*/
|
|
60
|
+
const consented = args.some((a) => a === "--yes" || a === "-y" || a === "--force");
|
|
61
|
+
let chatCount = 0;
|
|
62
|
+
let messageCount = 0;
|
|
63
|
+
if (ctx.tableExists(db, "chats")) {
|
|
64
|
+
chatCount = db.prepare("SELECT COUNT(*) AS n FROM chats WHERE agent_id=?").get(agent.id).n;
|
|
65
|
+
if (chatCount && ctx.tableExists(db, "chat_messages")) {
|
|
66
|
+
messageCount = db.prepare(
|
|
67
|
+
"SELECT COUNT(*) AS n FROM chat_messages WHERE chat_id IN (SELECT id FROM chats WHERE agent_id=?)",
|
|
68
|
+
).get(agent.id).n;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (chatCount && !consented) {
|
|
72
|
+
ctx.err(ko
|
|
73
|
+
? `"${agent.slug}" 제거는 대화 ${chatCount}개와 메시지 ${messageCount}개를 함께 영구 삭제합니다 (데스크탑 앱과 공유하는 DB이며 되돌릴 수 없습니다).\n계속하려면: agentlas uninstall ${agent.slug} --yes`
|
|
74
|
+
: `Uninstalling "${agent.slug}" will also permanently delete ${chatCount} chat(s) and ${messageCount} message(s) (shared with the Desktop app; this cannot be undone).\nRe-run to confirm: agentlas uninstall ${agent.slug} --yes`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
let deleted = false;
|
|
49
79
|
runWriteTransaction(db, () => {
|
|
50
80
|
deleted = db.prepare("DELETE FROM installed_agents WHERE id=?").run(agent.id).changes > 0;
|
|
@@ -58,7 +88,11 @@ function run(ctx, args) {
|
|
|
58
88
|
saveRoutes(routes);
|
|
59
89
|
}
|
|
60
90
|
} catch { /* 라우트 정리는 best-effort */ }
|
|
61
|
-
|
|
91
|
+
// 실제로 무엇이 사라졌는지 성공 줄에 남긴다 — 조용한 파괴 금지.
|
|
92
|
+
const cascade = chatCount
|
|
93
|
+
? (ko ? ` (대화 ${chatCount}개 · 메시지 ${messageCount}개 삭제됨)` : ` (deleted ${chatCount} chat(s), ${messageCount} message(s))`)
|
|
94
|
+
: "";
|
|
95
|
+
ctx.out(`${ctx.ui.green("✓")} ${ko ? "제거됨" : "Uninstalled"}: ${agent.slug}${cascade}`);
|
|
62
96
|
return 0;
|
|
63
97
|
}
|
|
64
98
|
ctx.err(ko ? "제거하지 못했습니다." : "Nothing was uninstalled.");
|
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* version — 터미널 버전과 함께 **지금 붙어 있는 Agentlas-OS Core** 를 보고한다.
|
|
4
|
+
*
|
|
5
|
+
* 왜 Core 도 같이 찍나 (2026-07-28):
|
|
6
|
+
* 터미널의 계약 표면(build/upload/workforce/context/…)은 Core 릴리스가 소유한다.
|
|
7
|
+
* Core 는 스스로 갱신되고(agentlas_cloud/update.py), 셸은 그때그때 해석된 루트에
|
|
8
|
+
* 붙는다. 그런데 `version`·`--where`·`doctor` 어디에서도 Core 를 찍지 않아서,
|
|
9
|
+
* 사용자도 지원 담당도 **어떤 Core 위에서 돌고 있는지 알 방법이 없었다**.
|
|
10
|
+
* "최신인데 안 맞는다" 류의 오진이 그 공백에서 나온다. 붙은 대상을 보여주는 것이
|
|
11
|
+
* 진단의 출발점이다.
|
|
12
|
+
*
|
|
13
|
+
* Core 가 없어도 실패하지 않는다 — 56개 명령은 Core 없이 동작한다. 없으면 없다고
|
|
14
|
+
* 정직하게 적는다(조용한 생략 금지).
|
|
15
|
+
*/
|
|
2
16
|
const { readVersion } = require("../agentlas-banner.cjs");
|
|
3
17
|
|
|
18
|
+
function coreLine() {
|
|
19
|
+
try {
|
|
20
|
+
const core = require("../agentlas-core-harness.cjs");
|
|
21
|
+
const root = core.resolveCoreRuntimeRoot();
|
|
22
|
+
if (!root) return "agentlas-os not attached (install: hephaestus update)";
|
|
23
|
+
const version = core.readCoreRuntimeVersion(root);
|
|
24
|
+
// 버전을 못 읽는 루트는 "낡음"이 아니라 "모름"이다. 두 상태를 구분해 적는다 —
|
|
25
|
+
// 모름을 그럴듯한 값으로 메꾸면 그 순간 진단이 거짓이 된다.
|
|
26
|
+
return `agentlas-os ${version || "unknown version"} (${root})`;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return `agentlas-os unavailable (${(error && error.message) || error})`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
4
32
|
function run(ctx) {
|
|
5
33
|
ctx.out(`agentlas ${readVersion()}`);
|
|
34
|
+
ctx.out(coreLine());
|
|
6
35
|
return 0;
|
|
7
36
|
}
|
|
8
37
|
|
|
@@ -11,17 +11,12 @@
|
|
|
11
11
|
* parity().cmdHep(["hep-network", ...]) 호환 탈출구였는데, 그 parity 모듈이 아직
|
|
12
12
|
* v2에 없으므로 정직 정지로 안내만 한다(가짜 성공 금지).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 경로를 만들었다. 그 프로젝트-상태 기계(폴더 방문 기록/활성화 판정)는 아직 v2로
|
|
17
|
-
* 포팅되지 않았다. cmdWorkforce 가 projectPath 를 쓰는 곳은 buildChildEnv 의
|
|
18
|
-
* 프로젝트 dotenv 스코핑뿐이고(agentlas-workforce.cjs 1926행) 부재 시 fail-closed
|
|
19
|
-
* 의존이 없음을 확인했다 — 그래서 여기서는 cwd 를 projectPath 로 넘긴다(같은 폴더
|
|
20
|
-
* 의 .env/.env.local 이 비신뢰 소스로 한 번 더 스캔될 뿐, 권한이 넓어지지 않는다).
|
|
21
|
-
* 프로젝트-상태 모듈이 v2에 착륙하면 이 자리를 그 결과로 교체한다.
|
|
14
|
+
* Write-capable first contact bootstraps the exact current project through the
|
|
15
|
+
* canonical Core merge-only boundary. Read-only inspection remains passive.
|
|
22
16
|
*/
|
|
23
17
|
const { workforceRuntime } = require("../workforce/deps.cjs");
|
|
24
18
|
const { projectCwd } = require("../workforce/capture.cjs");
|
|
19
|
+
const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
|
|
25
20
|
|
|
26
21
|
function resolvePermission(ctx) {
|
|
27
22
|
// v1 resolveTerminalPermission 포팅: 저장된 `full` 은 세션 한정 계약이라
|
|
@@ -70,11 +65,16 @@ async function dispatch(ctx, command, args) {
|
|
|
70
65
|
const db = ctx.db();
|
|
71
66
|
const cwd = projectCwd();
|
|
72
67
|
const permission = resolvePermission(ctx);
|
|
68
|
+
const projectPath = ensureTerminalProjectForExecutionCli(
|
|
69
|
+
db,
|
|
70
|
+
cwd,
|
|
71
|
+
permission,
|
|
72
|
+
`terminal-${command}`,
|
|
73
|
+
) || cwd;
|
|
73
74
|
const runtime = workforceRuntime({ lang: ctx.lang, out: ctx.out });
|
|
74
75
|
const result = await runtime.cmdWorkforce(db, rest, runtimeOverride, {
|
|
75
76
|
cwd,
|
|
76
|
-
|
|
77
|
-
projectPath: cwd,
|
|
77
|
+
projectPath,
|
|
78
78
|
permission,
|
|
79
79
|
});
|
|
80
80
|
return result && result.ok ? 0 : 1;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* core/schema-ensure — 공유 SQLite 에 대한 스키마 보정을 **커넥션당 한 번만** 한다.
|
|
4
|
+
*
|
|
5
|
+
* 왜 (2026-07-28 실측):
|
|
6
|
+
* `agentlas.sqlite` 는 데스크탑 앱과 공유하는 파일이다(제품 계약, `core/paths.cjs`).
|
|
7
|
+
* 그런데 터미널이 **매 턴** 그 파일에 DDL 을 날리고 있었다 —
|
|
8
|
+
* · `ALTER TABLE memory_entries ADD COLUMN context_json` (사본 3벌)
|
|
9
|
+
* · `CREATE TABLE IF NOT EXISTS terminal_memory_*` 4개 + 인덱스 5개
|
|
10
|
+
* 전부 `IF NOT EXISTS`/조건부라 결과는 멱등이지만, **쓰기 락을 잡는 것은 매번**이다.
|
|
11
|
+
* 데스크탑이 마이그레이션 트랜잭션 중이면 15초 busy_timeout 을 소진하고, 호출부가
|
|
12
|
+
* 전부 빈 catch 라 그 실패가 조용히 사라진다.
|
|
13
|
+
*
|
|
14
|
+
* 스키마 소유자는 데스크탑이다(`core/db.cjs:7-9`). 터미널의 이 보정은 "앱보다 먼저
|
|
15
|
+
* 깔린 DB" 같은 경우를 위한 안전망이지 상시 작업이 아니다. 프로세스가 사는 동안
|
|
16
|
+
* 한 번이면 충분하다.
|
|
17
|
+
*
|
|
18
|
+
* 왜 커넥션당인가 (프로세스당이 아니라):
|
|
19
|
+
* 테스트와 일부 명령이 임시 DB 를 따로 연다. 프로세스 단위로 기억하면 두 번째
|
|
20
|
+
* DB 가 보정을 건너뛰어 조용히 깨진다. WeakMap 이면 커넥션이 사라질 때 같이 사라진다.
|
|
21
|
+
*/
|
|
22
|
+
const applied = new WeakMap();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `key` 작업을 이 커넥션에서 아직 안 했으면 실행한다.
|
|
26
|
+
* 실패는 삼키되 **기억하지 않는다** — 다음 호출이 다시 시도할 수 있어야 한다.
|
|
27
|
+
* (일시적 락 경합으로 실패한 것을 "완료"로 기억하면 스키마가 영구히 안 맞는다.)
|
|
28
|
+
*/
|
|
29
|
+
function ensureOnce(db, key, fn) {
|
|
30
|
+
if (!db) return false;
|
|
31
|
+
let done = applied.get(db);
|
|
32
|
+
if (!done) {
|
|
33
|
+
done = new Set();
|
|
34
|
+
applied.set(db, done);
|
|
35
|
+
}
|
|
36
|
+
if (done.has(key)) return true;
|
|
37
|
+
try {
|
|
38
|
+
fn(db);
|
|
39
|
+
done.add(key);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tableExists(db, name) {
|
|
47
|
+
try {
|
|
48
|
+
return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name));
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function columnExists(db, table, column) {
|
|
55
|
+
try {
|
|
56
|
+
return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `memory_entries.context_json` 보정. 이전에는 이 함수가 세 파일에 각각 복제돼 있었고
|
|
64
|
+
* (`sessions/prompt.cjs`, `project/memory-context.cjs`, `memory-cli/curate.cjs`)
|
|
65
|
+
* 그중 하나는 매 턴 호출됐다. 하나로 합치고 커넥션당 1회로 줄인다.
|
|
66
|
+
*/
|
|
67
|
+
function ensureMemoryContextColumn(db) {
|
|
68
|
+
return ensureOnce(db, "memory_entries.context_json", (conn) => {
|
|
69
|
+
if (tableExists(conn, "memory_entries") && !columnExists(conn, "memory_entries", "context_json")) {
|
|
70
|
+
conn.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { ensureOnce, ensureMemoryContextColumn, tableExists, columnExists };
|
|
@@ -139,6 +139,37 @@ function resolveVariantCandidates(options) {
|
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/*
|
|
143
|
+
* usage/설명은 사람 출력 전용이다 — JSON 결과(schemaVersion v1)와 decision 어휘는
|
|
144
|
+
* 손대지 않는다. README·`agentlas help`가 광고하는 `agentlas variant resolve`는
|
|
145
|
+
* 인자 없이 부르면 항상 decision=error 로 끝나는데, 그동안 사람에게 보인 건
|
|
146
|
+
* 내부 enum 한 줄(`error: EXACT_BASE_RELEASE_REQUIRED`) 뿐이라 필요한 플래그
|
|
147
|
+
* 이름이 소스 주석에만 존재했다. 그래서 (1) error 코드마다 사람이 읽고 바로
|
|
148
|
+
* 고칠 수 있는 사유 + usage 를 붙이고, (2) help 서브커맨드를 뚫는다.
|
|
149
|
+
*/
|
|
150
|
+
const VARIANT_USAGE_LINES = Object.freeze([
|
|
151
|
+
"usage: agentlas variant resolve [candidates.json] [--base-release <agent-release-id>]",
|
|
152
|
+
" [--prefer <variant-id>] [--no-base-only] [--json]",
|
|
153
|
+
" --base-release <id> required: the exact base agent release to resolve against.",
|
|
154
|
+
" May instead come from candidates.json's baseAgentReleaseId.",
|
|
155
|
+
" candidates.json variant candidates: a JSON array, or {baseAgentReleaseId, candidates[]}.",
|
|
156
|
+
" --prefer <id> preferred variant; picking another one reports decision=fallback.",
|
|
157
|
+
" --no-base-only fail instead of falling back to the base release with no Experience Pack.",
|
|
158
|
+
" --json emit the agentlas.terminal-variant-resolution.v1 document.",
|
|
159
|
+
"Local advisory preview only: it never authorizes execution, rental, or reputation.",
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const VARIANT_ERROR_HINTS = Object.freeze({
|
|
163
|
+
EXACT_BASE_RELEASE_REQUIRED:
|
|
164
|
+
"no base agent release was given — pass --base-release <agent-release-id>, or a candidates.json carrying baseAgentReleaseId.",
|
|
165
|
+
NO_ELIGIBLE_VARIANT_AND_NO_BASE_FALLBACK:
|
|
166
|
+
"no candidate survived the checks above and --no-base-only forbids the base-only fallback.",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function variantUsage() {
|
|
170
|
+
return VARIANT_USAGE_LINES.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
142
173
|
function renderVariantResolution(result) {
|
|
143
174
|
const lines = [
|
|
144
175
|
`VARIANT RESOLUTION: ${result.decision}`,
|
|
@@ -148,17 +179,29 @@ function renderVariantResolution(result) {
|
|
|
148
179
|
if (result.decision === "selected") lines.push(`selected: ${result.selectedVariantId}`);
|
|
149
180
|
else if (result.decision === "fallback") lines.push(`fallback selected: ${result.selectedVariantId}`);
|
|
150
181
|
else if (result.decision === "base-only") lines.push(`base-only: ${result.baseAgentReleaseId} (no Experience Pack attached)`);
|
|
151
|
-
else
|
|
182
|
+
else {
|
|
183
|
+
lines.push(`error: ${result.code}`);
|
|
184
|
+
const hint = VARIANT_ERROR_HINTS[result.code];
|
|
185
|
+
if (hint) lines.push(hint);
|
|
186
|
+
}
|
|
152
187
|
if (result.fallbackOrder.length) lines.push(`next fallbacks: ${result.fallbackOrder.join(", ")}`);
|
|
153
188
|
for (const excluded of result.excluded) lines.push(`excluded only ${excluded.variantId}: ${excluded.reasons.join(", ")}`);
|
|
154
189
|
lines.push("Required MCP shortages exclude only the affected variant; they never create an agent-wide shortage.");
|
|
190
|
+
// 실패했을 때만 usage를 붙인다 — 성공 출력은 v1과 바이트 동일하게 유지한다.
|
|
191
|
+
if (result.decision === "error") lines.push("", variantUsage());
|
|
155
192
|
return lines.join("\n");
|
|
156
193
|
}
|
|
157
194
|
|
|
158
195
|
function cmdVariant(options) {
|
|
159
196
|
const args = options.args || [];
|
|
160
197
|
const sub = args[0] || "resolve";
|
|
161
|
-
|
|
198
|
+
// help 탈출구: 상위 라우터가 -h/--help 를 "help" 로 정규화하지만, 직접 호출도
|
|
199
|
+
// 받도록 셋 다 인정한다. usage는 성공(exit 0)이지 알 수 없는 서브커맨드가 아니다.
|
|
200
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
201
|
+
(options.out || console.log)(variantUsage());
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
if (sub !== "resolve") throw new Error(`unknown variant subcommand: ${sub} (resolve|help)`);
|
|
162
205
|
const flags = parseSimpleFlags(args.slice(1));
|
|
163
206
|
let candidates = [];
|
|
164
207
|
let baseAgentReleaseId = flags["base-release"] || null;
|
|
@@ -192,5 +235,6 @@ module.exports = {
|
|
|
192
235
|
validateVariantCandidate,
|
|
193
236
|
resolveVariantCandidates,
|
|
194
237
|
renderVariantResolution,
|
|
238
|
+
variantUsage,
|
|
195
239
|
cmdVariant,
|
|
196
240
|
};
|
|
@@ -40,6 +40,13 @@ const USAGE = Object.freeze({
|
|
|
40
40
|
call: 'usage: agentlas call "<agent-slugs>" "<context>"',
|
|
41
41
|
connect: "usage: agentlas connect [status|telegram|help]",
|
|
42
42
|
hep: "usage: agentlas hep <subcommand> [args]",
|
|
43
|
+
// 소스 스코프를 지키는 스태핑 표면 3종. 이름이 곧 계약이라 usage 도 스코프를
|
|
44
|
+
// 문장으로 적는다 — 이전에는 별칭이 스코프를 버려 hep-cloud 가 자산 보관함
|
|
45
|
+
// usage 를, hep-hub 가 검색 목록을 뱉었다(2026-07-28 수리).
|
|
46
|
+
"hep-network": 'usage: agentlas hep-network "<request>" # Local + owner Cloud + public Hub, federated by Core',
|
|
47
|
+
"hep-local": 'usage: agentlas hep-local "<request>" # registered Local agents only',
|
|
48
|
+
"hep-cloud": 'usage: agentlas hep-cloud "<request>" # owner Agent Cloud agents only',
|
|
49
|
+
"hep-hub": 'usage: agentlas hep-hub "<request>" # public Agentlas Hub agents only',
|
|
43
50
|
hephaestus: "usage: agentlas hephaestus <subcommand> [args]",
|
|
44
51
|
journal: "usage: agentlas journal <status|verify|repair|gate> --run-id <id> | --journal <path>",
|
|
45
52
|
"legacy-network": 'usage: agentlas legacy-network "<request>"',
|
|
@@ -31,13 +31,9 @@ function loadArch() {
|
|
|
31
31
|
return _arch;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
|
|
38
|
-
}
|
|
39
|
-
} catch { /* ignore */ }
|
|
40
|
-
}
|
|
34
|
+
// 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다 — 이 보정이 매 턴 공유 DB 에
|
|
35
|
+
// 쓰기 락을 잡고 있었다(2026-07-28). 소유자는 데스크탑 스키마다.
|
|
36
|
+
const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
|
|
41
37
|
|
|
42
38
|
const SECRET_RE = [/\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}/, /AKIA[0-9A-Z]{16}/, /ghp_[A-Za-z0-9]{20,}/, /xox[baprs]-[A-Za-z0-9-]{10,}/, /-----BEGIN [A-Z ]*PRIVATE KEY-----/, /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i];
|
|
43
39
|
|
|
@@ -32,13 +32,9 @@ function sha(value) {
|
|
|
32
32
|
return crypto.createHash("sha256").update(value).digest("hex");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
|
|
39
|
-
}
|
|
40
|
-
} catch { /* ignore */ }
|
|
41
|
-
}
|
|
35
|
+
// 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다 — 이 보정이 매 턴 공유 DB 에
|
|
36
|
+
// 쓰기 락을 잡고 있었다(2026-07-28). 소유자는 데스크탑 스키마다.
|
|
37
|
+
const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
|
|
42
38
|
|
|
43
39
|
function prefsLangCli() {
|
|
44
40
|
try {
|
package/engine/project/state.cjs
CHANGED
|
@@ -341,15 +341,16 @@ function activeProjectPath(db, options = {}) {
|
|
|
341
341
|
return result.activated ? root : null;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
// v1 시그니처 보존: permission/reason은 받되 절대 초기화 권한으로 쓰지 않는다.
|
|
345
344
|
function ensureTerminalProjectForExecutionCli(db, projectPath, permission = "write", reason = "terminal-first-contact") {
|
|
346
345
|
const root = terminalProjectCandidateCli(projectPath);
|
|
347
346
|
if (!root) return null;
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
347
|
+
// A real write-capable execution is the product's first-contact boundary.
|
|
348
|
+
// Install the same private, merge-only Core infrastructure in whichever
|
|
349
|
+
// project the user opened; never key this behavior to the Agentlas_F repo.
|
|
350
|
+
if (permission === "write" || permission === "full") {
|
|
351
|
+
return initializeTerminalProjectCli(db, root, reason);
|
|
352
|
+
}
|
|
353
|
+
// Read-only inspection remains passive and never creates files.
|
|
353
354
|
const active = activeProjectPath(db, { projectPath: root });
|
|
354
355
|
if (!active) return null;
|
|
355
356
|
return initializedAgentlasProjectPathCli(root);
|
|
@@ -117,6 +117,27 @@ class Orchestrator extends EventEmitter {
|
|
|
117
117
|
if (!session) return;
|
|
118
118
|
if (session.isBusy()) session.kill();
|
|
119
119
|
this.sessions.delete(key);
|
|
120
|
+
|
|
121
|
+
// 부모를 지워도 자식 세션은 살아 있다(실행 중이며 병렬 슬롯을 쥔 채일 수 있다).
|
|
122
|
+
// 자식의 parent 포인터가 지워진 세션을 계속 가리키면 list()의 루트 판정
|
|
123
|
+
// (!s.parent)에서 탈락하고, 어떤 루트에서도 닿지 않아 /sessions·/tree 에서
|
|
124
|
+
// 통째로 사라진다 — 실행 중인데 보이지도 끌 수도 없는 유령 세션이 된다.
|
|
125
|
+
// 그래서 세션이 맵을 떠날 때 트리를 함께 정리한다: 남은 자식은 살아 있는
|
|
126
|
+
// 조부모로 승계하고(없으면 루트로 승격), 지워진 키는 부모 목록에서 뗀다.
|
|
127
|
+
const grandparent = session.parent && this.sessions.has(session.parent.key) ? session.parent : null;
|
|
128
|
+
for (const childKey of session.children) {
|
|
129
|
+
const child = this.sessions.get(childKey);
|
|
130
|
+
if (!child) continue;
|
|
131
|
+
child.parent = grandparent;
|
|
132
|
+
if (grandparent && !grandparent.children.includes(childKey)) grandparent.children.push(childKey);
|
|
133
|
+
}
|
|
134
|
+
session.children = [];
|
|
135
|
+
if (session.parent) {
|
|
136
|
+
const siblings = session.parent.children;
|
|
137
|
+
const at = siblings.indexOf(key);
|
|
138
|
+
if (at >= 0) siblings.splice(at, 1);
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
if (this.activeKey === key) {
|
|
121
142
|
const rest = [...this.sessions.keys()];
|
|
122
143
|
this.activeKey = rest.length ? rest[rest.length - 1] : null;
|
|
@@ -124,14 +145,31 @@ class Orchestrator extends EventEmitter {
|
|
|
124
145
|
this.emit("sessions-changed");
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
/**
|
|
149
|
+
* 전 세션 브로드캐스트 — {sent, skipped}를 반환한다(throw 하지 않는다).
|
|
150
|
+
*
|
|
151
|
+
* WHY: 예전엔 루프 안에서 sendTo()의 throw가 그대로 올라갔다. 동시 상한(기본 4)보다
|
|
152
|
+
* 유휴 세션이 많으면 앞의 몇 개는 이미 프롬프트를 받아 실행을 시작한 뒤 상한 세션에서
|
|
153
|
+
* 터졌고, 그때까지 모은 sent 배열은 스택과 함께 버려졌다. 호출자(REPL)는 상한 에러
|
|
154
|
+
* 한 줄만 찍어 "전부 실패"로 보고했지만 실제로는 4개 세션이 그 지시를 받아 토큰을
|
|
155
|
+
* 쓰고 쓰기 작업까지 할 수 있는 상태였다 — 부분 성공을 전면 실패로 오보하는 것은
|
|
156
|
+
* 브로드캐스트에서 가장 위험한 거짓말이다.
|
|
157
|
+
* 그래서 실패는 세션 단위로 모으고, 실제 전달된 목록은 무슨 일이 있어도 반환한다.
|
|
158
|
+
* (sendTo/spawn의 "상한 초과는 정직한 거부" 계약 자체는 그대로 둔다.)
|
|
159
|
+
*/
|
|
127
160
|
broadcast(prompt) {
|
|
128
161
|
const sent = [];
|
|
162
|
+
const skipped = [];
|
|
129
163
|
for (const [key, session] of this.sessions) {
|
|
130
164
|
if (session.status === "killed") continue;
|
|
131
|
-
|
|
132
|
-
|
|
165
|
+
try {
|
|
166
|
+
this.sendTo(key, prompt);
|
|
167
|
+
sent.push(key);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
skipped.push({ key, error: String((e && e.message) || e) });
|
|
170
|
+
}
|
|
133
171
|
}
|
|
134
|
-
return sent;
|
|
172
|
+
return { sent, skipped };
|
|
135
173
|
}
|
|
136
174
|
|
|
137
175
|
/** 세션 표: [{key, active, agent, status, elapsed, lastLine, parentKey, depth}] */
|
|
@@ -88,13 +88,10 @@ function contextLine(json) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
} catch { /* 앱 마이그레이션 중이면 다음 턴에 */ }
|
|
97
|
-
}
|
|
91
|
+
// 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다. 이 파일의 호출부는 **매 턴** 도는
|
|
92
|
+
// 자리라(cliMemoryContext), 여기가 공유 DB 에 쓰기 락을 가장 자주 잡던 지점이었다.
|
|
93
|
+
// 실패는 여전히 다음 호출에서 재시도된다 — 실패를 "완료"로 기억하지 않는다.
|
|
94
|
+
const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
|
|
98
95
|
|
|
99
96
|
/** Context Map 슬라이스 — Core 부재 시 정직하게 빈 문자열 (조작된 지도 금지). */
|
|
100
97
|
function cliProjectContextSlice(projectPath, task) {
|
|
@@ -124,6 +124,25 @@ class Session extends EventEmitter {
|
|
|
124
124
|
return result;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* 이미 동의된 시스템 MCP 서버 목록. `full` 권한이 아니면 빈 배열이다.
|
|
129
|
+
*
|
|
130
|
+
* 권한 확인을 여기서 한 번 더 하는 이유: native-host 도 `full` 게이트를 갖고
|
|
131
|
+
* 있지만, 권한이 낮은 턴에서 목록을 **읽는 것 자체를** 하지 않는 편이 낫다.
|
|
132
|
+
* 읽지 않은 값은 실수로 새 나갈 수 없다.
|
|
133
|
+
*/
|
|
134
|
+
_consentedMcpServers() {
|
|
135
|
+
if (permissions.normalize(this.permission) !== "full") return [];
|
|
136
|
+
try {
|
|
137
|
+
const consent = require("../mcp/consent.cjs");
|
|
138
|
+
return consent.readConsentedSystemMcpServers(this.db, { env: process.env }) || [];
|
|
139
|
+
} catch {
|
|
140
|
+
// 동의 상태를 못 읽으면 MCP 없이 간다. 여기서 실패해 턴을 죽이면,
|
|
141
|
+
// MCP 는 부가 기능인데 대화 자체가 불가능해진다.
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
async _runTurn(prompt) {
|
|
128
147
|
this.status = "running";
|
|
129
148
|
this.startedAt = Date.now();
|
|
@@ -196,6 +215,19 @@ class Session extends EventEmitter {
|
|
|
196
215
|
session: { ...this.runtimeSession },
|
|
197
216
|
model: this.runtime.model,
|
|
198
217
|
effort: this.runtime.effort,
|
|
218
|
+
// 사용자가 이미 동의한 MCP 서버를 턴에 싣는다.
|
|
219
|
+
//
|
|
220
|
+
// 여기가 비어 있어서 챗 경로의 MCP 가 통째로 죽어 있었다(2026-07-28 확인:
|
|
221
|
+
// `mcpServers` 를 넘기는 호출자가 0곳). native-host 는 `full` 권한에서만
|
|
222
|
+
// 주입하도록 이미 게이팅돼 있었는데, 그 분기에 도달할 데이터를 아무도
|
|
223
|
+
// 채우지 않았다 — `agentlas mcp probe` 가 connected 를 찍어도 실제 턴에서는
|
|
224
|
+
// 그 서버를 쓸 수 없었다.
|
|
225
|
+
//
|
|
226
|
+
// 여기서 새로 묻지 않는다. `readConsentedSystemMcpServers` 는 이미 받아 둔
|
|
227
|
+
// 동의 영수증과 지문이 **지금도 일치하는** 서버만 돌려준다. 동의가 없으면
|
|
228
|
+
// 빈 배열이고, native-host 가 명시적 빈 격리로 간다. 턴 도중에 동의를 묻는
|
|
229
|
+
// 것은 사용자가 답할 수 없는 자리에서 묻는 것이라 하지 않는다.
|
|
230
|
+
mcpServers: this._consentedMcpServers(),
|
|
199
231
|
onSpawn: (child) => { this._child = child; },
|
|
200
232
|
};
|
|
201
233
|
if (this._spawnImpl) req.spawn = this._spawnImpl;
|
package/engine/storm/swarm.cjs
CHANGED
|
@@ -427,6 +427,18 @@ function create(deps) {
|
|
|
427
427
|
if (args[i] === "--parallel" || args[i] === "-n") concurrency = Number(args[++i]);
|
|
428
428
|
else rest.push(args[i]);
|
|
429
429
|
}
|
|
430
|
+
// storm.cjs stormRun 의 leading-dash 가드와 동일 계약. 여기서 안 막으면 미지원·
|
|
431
|
+
// 오타 플래그(--permission, --model …)가 rest 에 남아 rest.join(" ") 로 목표가
|
|
432
|
+
// 되어버린다: 유료 플래너가 플래그 텍스트를 목표로 실제 실행되고, 플래그가
|
|
433
|
+
// 의도한 동작(예: 권한 상승)은 조용히 일어나지 않는다. 목표는 프롬프트까지
|
|
434
|
+
// 오염된다. 그래서 소비되지 않은 대시 토큰은 실행 전에 fail-closed 로 거절한다.
|
|
435
|
+
const strayFlag = rest.find((token) => String(token).startsWith("-"));
|
|
436
|
+
if (strayFlag) {
|
|
437
|
+
const ui = executionContext.ui || newUi();
|
|
438
|
+
ui.error(`unknown option ${strayFlag} — swarm accepts: --parallel N | -n N, --runtime <kind>`);
|
|
439
|
+
process.exitCode = 1;
|
|
440
|
+
return { ok: false, error: "unknown-option" };
|
|
441
|
+
}
|
|
430
442
|
const r = await swarmRun(db, rest.join(" "), { ...executionContext, concurrency, runtimeOverride });
|
|
431
443
|
if (!r.ok) process.exitCode = 1;
|
|
432
444
|
return r;
|
package/engine/ui/palette.cjs
CHANGED
|
@@ -44,6 +44,13 @@ const SLASH_COMMANDS = [
|
|
|
44
44
|
{ command: "/network", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
|
|
45
45
|
{ command: "/workforce", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
|
|
46
46
|
{ command: "/taskforce", args: "<request>", ko: "임시 태스크포스 편성", en: "Assemble a task force" },
|
|
47
|
+
// 소스 스코프가 이름에 붙은 스태핑 3종. 스코프를 문장에 적는다 — 예전에는
|
|
48
|
+
// 별칭이 스코프를 버려 /hep-cloud 가 자산 보관함으로, /hep-hub 가 검색으로
|
|
49
|
+
// 갔다(2026-07-28 수리). 팔레트 문구가 곧 사용자에게 하는 약속이다.
|
|
50
|
+
{ command: "/hep-network", args: "\"<request>\"", ko: "로컬+오너 클라우드+공개 Hub 연합 편성", en: "Staff across Local + owner Cloud + public Hub" },
|
|
51
|
+
{ command: "/hep-local", args: "\"<request>\"", ko: "등록된 로컬 에이전트만으로 편성", en: "Staff from registered Local agents only" },
|
|
52
|
+
{ command: "/hep-cloud", args: "\"<request>\"", ko: "오너 Agent Cloud만으로 편성", en: "Staff from owner Agent Cloud only" },
|
|
53
|
+
{ command: "/hep-hub", args: "\"<request>\"", ko: "공개 Hub 에이전트만으로 편성", en: "Staff from public Hub agents only" },
|
|
47
54
|
{ command: "/build", args: "\"<request>\"", ko: "에이전트·팀 제작/수리/패키징", en: "Build, repair or package an agent or team" },
|
|
48
55
|
{ command: "/call", args: "\"a,b\" \"<ctx>\"", ko: "지정 에이전트 호출", en: "Call named agents" },
|
|
49
56
|
{ command: "/route", args: "\"<req>\"", ko: "최적 에이전트 라우팅", en: "Route to the best agent" },
|
|
@@ -61,7 +68,11 @@ const SLASH_COMMANDS = [
|
|
|
61
68
|
{ command: "/variant", args: "resolve", ko: "로컬 변형 선택", en: "Local variant selection" },
|
|
62
69
|
{ command: "/memory", args: "<sub>", ko: "메모리", en: "Memory" },
|
|
63
70
|
{ command: "/evolve", args: "", ko: "프롬프트 진화 제안", en: "Prompt-evolution proposals" },
|
|
64
|
-
|
|
71
|
+
// 데스크탑의 `ontology` 는 Core 의 지식·메모리 **런타임**(임베딩 포함)이고, 터미널의
|
|
72
|
+
// 이것은 **이 프로젝트의 지식 소스 등록부**다. 서로 다른 것이 같은 이름을 쓰고 있어
|
|
73
|
+
// (감사 D6) 라벨이라도 정확해야 한다 — 명령 이름은 사용자 습관과 스크립트가 걸려
|
|
74
|
+
// 있어 바꾸지 않는다. Core 의 지식 런타임은 터미널에 아직 미노출이다(결함 아님).
|
|
75
|
+
{ command: "/ontology", args: "", ko: "프로젝트 지식 소스 등록", en: "Project knowledge sources" },
|
|
65
76
|
{ command: "/career-graph", args: "", ko: "소스 라우팅 그래프", en: "Source routing graph" },
|
|
66
77
|
{ command: "/journal", args: "<sub>", ko: "Stormbreaker 실행 일지", en: "Stormbreaker run journal" },
|
|
67
78
|
{ command: "/project", args: "[status|init]", ko: ".agentlas 프로젝트 상태", en: "Private project state" },
|
|
@@ -77,7 +88,8 @@ const SLASH_COMMANDS = [
|
|
|
77
88
|
{ command: "/update", args: "", ko: "npm 업데이트 확인", en: "npm update check" },
|
|
78
89
|
{ command: "/version", args: "", ko: "버전", en: "Version" },
|
|
79
90
|
{ command: "/logout", args: "", ko: "로그아웃", en: "Sign out" },
|
|
80
|
-
|
|
91
|
+
// 대화가 있으면 --yes 없이는 거절한다(챗/메시지 CASCADE 삭제) — 팔레트에도 노출.
|
|
92
|
+
{ command: "/uninstall", args: "<slug> [--yes]", ko: "에이전트 제거", en: "Uninstall an agent" },
|
|
81
93
|
{ command: "/quit", args: "", ko: "종료", en: "Quit" },
|
|
82
94
|
{ command: "/exit", args: "", ko: "종료", en: "Quit" },
|
|
83
95
|
];
|
package/engine/ui/renderer.cjs
CHANGED
|
@@ -77,6 +77,43 @@ class Renderer {
|
|
|
77
77
|
case "task-result": ui.applyTaskResult(ev.name, ev.result, ev.id); return;
|
|
78
78
|
case "cost": ui.cost(ev.usage); return;
|
|
79
79
|
case "queued": ui.line(ui.c.dim(` ↳ queued for next turn: ${ev.text}`)); return;
|
|
80
|
+
|
|
81
|
+
/* ── 펜스 영수증 (apply-fences.cjs) ──────────────────────────────────
|
|
82
|
+
* WHY: 펜스 블록은 cleanText 에서 통째로 제거되므로, 이 case 들이 없으면
|
|
83
|
+
* default 로 떨어져 화면에 아무 흔적도 남지 않는다. 그 결과 이미 enabled
|
|
84
|
+
* 상태로 등록된 반복 자동화(데몬/데스크탑 스케줄러가 실제로 실행한다)나
|
|
85
|
+
* 스폰된 위임 세션이 사용자 모르게 생기고, 에이전트의 확인 질문(ask)은
|
|
86
|
+
* 본문에서 삭제된 채 영영 묻지 않는다. 부작용에는 반드시 영수증이 따른다 —
|
|
87
|
+
* 새 펜스 이벤트를 apply-fences 에 추가하면 여기에도 case 를 추가할 것. */
|
|
88
|
+
case "ask": {
|
|
89
|
+
const p = ev.payload || {};
|
|
90
|
+
ui.ensureNl();
|
|
91
|
+
if (p.header) ui.line(ui.c.dim(` ${p.header}`));
|
|
92
|
+
ui.warn(p.question || "(question)");
|
|
93
|
+
(p.options || []).forEach((o, i) => {
|
|
94
|
+
const desc = o && o.description ? ` — ${o.description}` : "";
|
|
95
|
+
ui.line(ui.c.dim(` ${i + 1}. ${(o && o.label) || ""}${desc}`));
|
|
96
|
+
});
|
|
97
|
+
ui.line(ui.c.dim(` ↳ 답을 그대로 입력하세요${p.multiSelect ? " (복수 선택 가능)" : ""}`));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
case "automation-registered": {
|
|
101
|
+
const steps = ev.stepsIgnored ? ` · steps ${ev.stepsIgnored}개 무시(터미널은 그래프 합성 없음)` : "";
|
|
102
|
+
ui.ok(`automation registered: ${ev.name} · ${ev.schedule} · next ${ev.nextRunAt}${steps}`);
|
|
103
|
+
// 취소 경로를 함께 제시한다. 실제 서브커맨드는 off (commands/automation.cjs:125).
|
|
104
|
+
ui.line(ui.c.dim(` ↳ agentlas automation list · agentlas automation off ${String(ev.id || "").slice(0, 8)}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
case "automation-refused": ui.warn(`automation refused: ${ev.name} — ${ev.reason}`); return;
|
|
108
|
+
case "delegate-spawned":
|
|
109
|
+
ui.ok(`delegate spawned: ${ev.target} → ${ev.key}`);
|
|
110
|
+
return;
|
|
111
|
+
case "delegate-refused": ui.warn(`delegate refused: ${ev.target} — ${ev.reason}`); return;
|
|
112
|
+
case "fence-error": ui.error(`fence: ${ev.text}`); return;
|
|
113
|
+
case "memory-curated":
|
|
114
|
+
ui.line(ui.c.dim(` ↳ memory: ${ev.written}/${ev.candidates} written (permission: ${ev.permission})`));
|
|
115
|
+
return;
|
|
116
|
+
|
|
80
117
|
default: return;
|
|
81
118
|
}
|
|
82
119
|
}
|
package/engine/ui/repl.cjs
CHANGED
|
@@ -616,8 +616,14 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
616
616
|
|
|
617
617
|
case "broadcast": {
|
|
618
618
|
if (!restStr) throw new Error("Usage: /broadcast <message>");
|
|
619
|
-
|
|
619
|
+
/*
|
|
620
|
+
* broadcast 는 {sent, skipped} 를 준다. 전달된 목록을 먼저 찍고 못 보낸 세션은
|
|
621
|
+
* 사유와 함께 경고로 덧붙인다 — 예전엔 상한 throw 가 이 catch 로 떨어져 에러 한
|
|
622
|
+
* 줄만 보였고, 이미 지시를 받아 돌기 시작한 세션이 화면에 전혀 안 나왔다.
|
|
623
|
+
*/
|
|
624
|
+
const { sent, skipped } = orch.broadcast(restStr);
|
|
620
625
|
ctx.out(ui.c.dim(`→ ${sent.join(", ") || "(none)"}`));
|
|
626
|
+
for (const s of skipped) ui.warn(`${s.key} ${en ? "not sent" : "미전송"}: ${s.error}`);
|
|
621
627
|
return;
|
|
622
628
|
}
|
|
623
629
|
|