agentlas 1.0.47 → 1.0.49

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +4 -2
  3. package/bin/agentlas.cjs +10 -0
  4. package/engine/acp/server.cjs +279 -0
  5. package/engine/agentlas-capabilities.cjs +2 -1
  6. package/engine/agentlas-input.cjs +2 -1
  7. package/engine/agentlas-native-host.cjs +6 -0
  8. package/engine/agentlas-onboard.cjs +2 -1
  9. package/engine/agentlas-sqlite-policy.cjs +9 -0
  10. package/engine/automation/daemon.cjs +3 -7
  11. package/engine/bootstrap-schema.sql +1037 -1010
  12. package/engine/cloud-assets/package.cjs +11 -3
  13. package/engine/cloud-assets/upload-scan-catalog.generated.cjs +102 -0
  14. package/engine/commands/acp.cjs +45 -0
  15. package/engine/commands/billing.cjs +2 -2
  16. package/engine/commands/call.cjs +4 -0
  17. package/engine/commands/doctor.cjs +2 -1
  18. package/engine/commands/index.cjs +2 -0
  19. package/engine/commands/workforce.cjs +11 -0
  20. package/engine/core/db.cjs +53 -1
  21. package/engine/core/desktop-core.cjs +108 -4
  22. package/engine/core/store-schema.cjs +119 -0
  23. package/engine/firms/orchestrate.cjs +32 -1
  24. package/engine/project/memory-context.cjs +7 -0
  25. package/engine/runtimes/acp-driver.cjs +96 -0
  26. package/engine/runtimes/detect.cjs +3 -13
  27. package/engine/runtimes/kinds.cjs +84 -0
  28. package/engine/runtimes/resolve.cjs +40 -7
  29. package/engine/ui/commands-catalog.cjs +2 -0
  30. package/engine/ui/palette.cjs +2 -1
  31. package/engine/ui/repl.cjs +2 -1
  32. package/engine/ui/shell.cjs +38 -3
  33. package/engine/vendor/desktop-core.manifest.json +5 -5
  34. package/engine/workforce/capture.cjs +4 -8
  35. package/engine/workforce/deps.cjs +7 -0
  36. package/package.json +2 -1
@@ -123,6 +123,13 @@ function cliProjectContextSlice(projectPath, task) {
123
123
  "--task-stdin",
124
124
  "--no-refresh",
125
125
  "--render",
126
+ // Recall degrades to a labelled map, never to nothing. Core's passive
127
+ // freshness check walks the whole repository (measured 11.0s on the
128
+ // pilot) against this 4s timeout, and any non-zero exit is swallowed
129
+ // into "" below — so without a budget a large project silently lost
130
+ // its slice on every turn.
131
+ "--allow-stale",
132
+ "--freshness-budget", "0.4",
126
133
  ],
127
134
  {
128
135
  cwd: projectPath,
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /*
3
+ * runtimes/acp-driver — kimi · grok · cursor 를 위한 ACP 드라이버 (PRD 2026-08-15 T-2).
4
+ *
5
+ * 세 번째 손코딩이 아니다. Desktop 이 만든 공용 ACP 러너(electron/runtime/acp.js)를 벤더 코어에서
6
+ * 그대로 로드해 쓴다 — 데스크탑·터미널이 같은 파일 하나로 같은 런타임을 같은 품질로 돈다.
7
+ * (터미널이 손으로 미러링해 온 native-host 4종과 달리 드리프트가 구조적으로 없다.)
8
+ *
9
+ * 벤더 코어에 acp.js 가 없으면(옛 코어) — 종전 그대로 "드라이버 없음"으로 정직하게 거부한다.
10
+ * 조용히 다른 런타임으로 넘어가지 않는다.
11
+ */
12
+ const { loadCoreAcpRuntime } = require("../core/desktop-core.cjs");
13
+
14
+ // 정본(runtimes/kinds.cjs)의 ACP 3종에서 파생 — resolve.cjs의 ACP_CLI_KINDS와 같은 원소.
15
+ const ACP_KINDS = new Set(require("./kinds.cjs").ACP_CLI_KINDS);
16
+
17
+ /** 이 머신에서 ACP 드라이버를 쓸 수 있는가. { ok, reason?, module? } */
18
+ function acpDriverAvailability() {
19
+ const loaded = loadCoreAcpRuntime();
20
+ if (!loaded) return { ok: false, reason: "desktop core not available (run `agentlas doctor`)" };
21
+ if (loaded.error) return { ok: false, reason: loaded.error.message };
22
+ if (!loaded.module || typeof loaded.module.createAcpRunner !== "function") {
23
+ return { ok: false, reason: "desktop core exposes no createAcpRunner" };
24
+ }
25
+ return { ok: true, module: loaded.module };
26
+ }
27
+
28
+ function acpSpecFor(kind, mod) {
29
+ const spec = mod.ACP_AGENTS && mod.ACP_AGENTS[kind];
30
+ return spec || null;
31
+ }
32
+
33
+ /**
34
+ * native-host 계약으로 ACP 턴을 돈다.
35
+ * req = { kind, bin, prompt, systemPrompt, cwd, permission, ui, env, signal, model, locale }
36
+ * 반환: { text, session, usage, error, errorKind, errorSource }
37
+ */
38
+ async function runAcpTurn(req) {
39
+ const { kind, bin, ui } = req;
40
+ const avail = acpDriverAvailability();
41
+ if (!avail.ok) {
42
+ return { text: "", session: req.session || {}, error: `runtime '${kind}' has no ACP driver here: ${avail.reason}`, errorKind: "unsupported", errorSource: "marker" };
43
+ }
44
+ const mod = avail.module;
45
+ const spec = acpSpecFor(kind, mod);
46
+ if (!spec) {
47
+ return { text: "", session: req.session || {}, error: `runtime '${kind}' is not an ACP agent in this core`, errorKind: "unsupported", errorSource: "marker" };
48
+ }
49
+ const runner = mod.createAcpRunner(spec);
50
+ const locale = req.locale === "ko" ? "ko" : "en";
51
+ let streaming = false;
52
+ let lastText = "";
53
+ const events = {
54
+ onPartial: (full) => {
55
+ const text = String(full || "");
56
+ if (!streaming) { ui.streamStart(); streaming = true; }
57
+ const delta = text.startsWith(lastText) ? text.slice(lastText.length) : text;
58
+ lastText = text;
59
+ if (delta) ui.streamDelta(delta);
60
+ },
61
+ onStatus: (status) => ui.status(status),
62
+ onTool: (name, args, result, id, isError) => {
63
+ ui.tool(name, args || "");
64
+ if (result) ui.toolResult(result, !isError);
65
+ },
66
+ onThinking: (phase) => { if (phase === "start") ui.status(locale === "ko" ? "생각 중..." : "thinking..."); },
67
+ onNotice: (notice) => { if (notice && notice.message) ui.status(notice.message); },
68
+ };
69
+ try {
70
+ const result = await runner({
71
+ systemPrompt: req.systemPrompt || "",
72
+ history: [],
73
+ userPrompt: req.prompt || "",
74
+ backendLabel: spec.label,
75
+ locale,
76
+ permission: req.permission,
77
+ runtimeSource: bin,
78
+ cwd: req.cwd,
79
+ env: req.env || process.env,
80
+ signal: req.signal,
81
+ ...(req.model ? { model: req.model } : {}),
82
+ }, events);
83
+ if (streaming) ui.streamEnd();
84
+ const session = { ...(req.session || {}), ...(result.sessionId ? { acpSessionId: result.sessionId } : {}) };
85
+ if (result.failure) {
86
+ return { text: result.text || "", session, usage: null, error: result.failure.message, errorKind: result.failure.kind, errorSource: result.failure.source };
87
+ }
88
+ return { text: result.text || "", session, usage: null, error: null };
89
+ } catch (e) {
90
+ if (streaming) ui.streamEnd();
91
+ const message = e && e.message ? e.message : String(e);
92
+ return { text: "", session: req.session || {}, usage: null, error: message, errorKind: /abort/i.test(message) ? "cancelled" : "exit", errorSource: "marker" };
93
+ }
94
+ }
95
+
96
+ module.exports = { ACP_KINDS, acpDriverAvailability, acpSpecFor, runAcpTurn };
@@ -7,20 +7,10 @@
7
7
  * (없으면 no_runtime 정직 정지 — 폴백 금지는 상위 계층의 계약).
8
8
  */
9
9
  const { spawnSync } = require("node:child_process");
10
+ // kind 목록/실행 파일 이름의 정본은 runtimes/kinds.cjs 하나다 — 여기서 다시 적지 않는다.
11
+ const { RUNTIME_BIN, CLI_KINDS } = require("./kinds.cjs");
10
12
 
11
- const RUNTIME_BIN = {
12
- "claude-code": "claude",
13
- codex: "codex",
14
- // Antigravity CLI — gemini 후속. 공식 gemini CLI가 계정 티어로 죽어도(IneligibleTierError,
15
- // 실측 2026-08-06) 이쪽은 산다. 데스크탑 gemini 러너의 agy 경로와 같은 실물.
16
- agy: "agy",
17
- gemini: "gemini",
18
- kimi: "kimi",
19
- grok: "grok",
20
- cursor: "cursor-agent",
21
- };
22
-
23
- const CLI_RUNTIMES = Object.keys(RUNTIME_BIN);
13
+ const CLI_RUNTIMES = CLI_KINDS;
24
14
 
25
15
  function whichSync(bin) {
26
16
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /*
3
+ * runtimes/kinds — 런타임 종류(RuntimeKind)의 단일 정본 (2026-08-18).
4
+ *
5
+ * 배경: 같은 어휘가 8곳(detect·capture·resolve·acp-driver·capabilities·onboard·
6
+ * palette·input)에 손 목록으로 흩어져 있었다 — 새 kind 를 추가하면 한두 곳이
7
+ * 반드시 빠진다. 여기 한 벌만 두고, 다른 표면은 이 상수를 **가져다 쓴다**.
8
+ * 좁은 목록(캡처 4종 등)도 정본에서 파생해야 새 kind 가 모든 표면에 보인다.
9
+ *
10
+ * 순서가 곧 계약이다:
11
+ * - detect.listAvailableCliRuntimes 는 이 순서로 PATH 를 훑고, resolve 의
12
+ * "detected" 폴백은 그 첫 항목을 쓴다.
13
+ * - 온보딩 위저드 선택지·팔레트/입력 완성 후보도 이 순서로 표시된다.
14
+ */
15
+
16
+ const RUNTIME_KIND_SPECS = [
17
+ { kind: "claude-code", bin: "claude", driver: "native", capture: true },
18
+ { kind: "codex", bin: "codex", driver: "native", capture: true },
19
+ // Antigravity CLI — gemini 후속. 공식 gemini CLI가 계정 티어로 죽어도(IneligibleTierError,
20
+ // 실측 2026-08-06) 이쪽은 산다. 데스크탑 gemini 러너의 agy 경로와 같은 실물.
21
+ { kind: "agy", bin: "agy", driver: "native", capture: true },
22
+ { kind: "gemini", bin: "gemini", driver: "native", capture: true },
23
+ // kimi/grok/cursor 는 ACP 드라이버(runtimes/acp-driver.cjs → 벤더 코어의 공용
24
+ // ACP 러너)로 돈다 (PRD 2026-08-15 T-2). 캡처(buildArgs/텍스트 추출) 계약은 없다.
25
+ { kind: "kimi", bin: "kimi", driver: "acp" },
26
+ { kind: "grok", bin: "grok", driver: "acp" },
27
+ { kind: "cursor", bin: "cursor-agent", driver: "acp" },
28
+ ];
29
+
30
+ /** kind → 실행 파일 이름. CLI 런타임 전체(네이티브 + ACP). */
31
+ const RUNTIME_BIN = Object.fromEntries(RUNTIME_KIND_SPECS.map((s) => [s.kind, s.bin]));
32
+
33
+ /** CLI 런타임 kind 전체(탐지 순서). */
34
+ const CLI_KINDS = RUNTIME_KIND_SPECS.map((s) => s.kind);
35
+
36
+ /** native-host 드라이버(스폰 러너)를 갖춘 CLI 4종. */
37
+ const NATIVE_CLI_KINDS = RUNTIME_KIND_SPECS.filter((s) => s.driver === "native").map((s) => s.kind);
38
+
39
+ /** 벤더 코어의 공용 ACP 러너로 도는 3종. */
40
+ const ACP_CLI_KINDS = RUNTIME_KIND_SPECS.filter((s) => s.driver === "acp").map((s) => s.kind);
41
+
42
+ /** 캡처(no-authority headless) 드라이버가 검증된 kind — buildArgs/텍스트 추출 계약 보유. */
43
+ const CAPTURE_CLI_KINDS = RUNTIME_KIND_SPECS.filter((s) => s.capture).map((s) => s.kind);
44
+
45
+ /** kind → bin, 캡처 검증본만. workforce/capture 가 쓴다. */
46
+ const CAPTURE_RUNTIME_BIN = Object.fromEntries(
47
+ RUNTIME_KIND_SPECS.filter((s) => s.capture).map((s) => [s.kind, s.bin]),
48
+ );
49
+
50
+ /** CLI 가 아니라 로컬 API loop 로 실행되는 kind. */
51
+ const API_EXECUTABLE_KINDS = ["ollama"];
52
+
53
+ /** BYOK/API 백엔드 spec 문자열(/runtime 완성 후보의 API 절반). */
54
+ const API_BACKEND_SPECS = ["anthropic", "openai", "google", "ollama", "upstage"];
55
+
56
+ /** /runtime 이 받는 spec 전체: 네이티브 CLI kind + API 백엔드. */
57
+ const RUNTIME_SPECS = [...NATIVE_CLI_KINDS, ...API_BACKEND_SPECS];
58
+
59
+ /**
60
+ * 저장 계약(automation 등)이 허용하는 kind 전체 — CLI + 로컬/BYOK 실행 kind.
61
+ * 데스크탑 shared/runtime-kinds.ts 와 동형(터미널 표기: antigravity→agy, acp 미지원).
62
+ */
63
+ const CONTRACT_RUNTIME_KINDS = [...CLI_KINDS, "byok", "ollama", "lmstudio", "mlx"];
64
+
65
+ /** 저장 계약이 허용하는 LLM 백엔드 — 데스크탑 shared/runtime-backends.ts 와 동일 15종. */
66
+ const CONTRACT_RUNTIME_BACKENDS = [
67
+ "anthropic", "openai", "google", "ollama", "lmstudio", "mlx", "upstage", "custom", "glm",
68
+ "kimi", "deepseek", "minimax", "xai", "openrouter", "cursor",
69
+ ];
70
+
71
+ module.exports = {
72
+ RUNTIME_KIND_SPECS,
73
+ RUNTIME_BIN,
74
+ CLI_KINDS,
75
+ NATIVE_CLI_KINDS,
76
+ ACP_CLI_KINDS,
77
+ CAPTURE_CLI_KINDS,
78
+ CAPTURE_RUNTIME_BIN,
79
+ API_EXECUTABLE_KINDS,
80
+ API_BACKEND_SPECS,
81
+ RUNTIME_SPECS,
82
+ CONTRACT_RUNTIME_KINDS,
83
+ CONTRACT_RUNTIME_BACKENDS,
84
+ };
@@ -6,17 +6,45 @@
6
6
  * 아무것도 없으면 no_runtime "정직 정지" — 키워드/저품질 폴백 금지(오너 결정).
7
7
  */
8
8
  const { RUNTIME_BIN, whichSync, listAvailableCliRuntimes, activeRuntimeRow } = require("./detect.cjs");
9
+ const KINDS = require("./kinds.cjs");
9
10
  const path = require("node:path");
10
11
 
11
12
  // Session이 실제 드라이버를 갖춘 런타임만 실행 대상으로 삼는다.
12
13
  // CLI는 native-host, Ollama는 로컬 API loop를 쓴다. 다른 드라이버가 포팅되면
13
14
  // 해당 집합에 추가한다(조용한 오폭 방지).
14
- const CLI_EXECUTABLE_KINDS = new Set(["claude-code", "codex", "gemini", "agy"]);
15
- const API_EXECUTABLE_KINDS = new Set(["ollama"]);
16
- const EXECUTABLE_KINDS = new Set([
17
- ...CLI_EXECUTABLE_KINDS,
18
- ...API_EXECUTABLE_KINDS,
19
- ]);
15
+ //
16
+ // kimi/grok/cursor ACP 드라이버(runtimes/acp-driver.cjs → 벤더 코어의 공용 ACP 러너)로 돈다
17
+ // (PRD 2026-08-15 T-2). 벤더 코어가 그 러너를 갖고 있을 때만 실행 대상에 든다 — 옛 코어면
18
+ // 종전과 같은 "드라이버 없음" 정직 거부.
19
+ // 집합의 원소는 정본(runtimes/kinds.cjs)에서 파생한다 — 여기서 다시 적지 않는다.
20
+ const NATIVE_CLI_KINDS = new Set(KINDS.NATIVE_CLI_KINDS);
21
+ const ACP_CLI_KINDS = new Set(KINDS.ACP_CLI_KINDS);
22
+ const API_EXECUTABLE_KINDS = new Set(KINDS.API_EXECUTABLE_KINDS);
23
+
24
+ function acpKindsAvailable() {
25
+ try {
26
+ const { acpDriverAvailability } = require("./acp-driver.cjs");
27
+ return acpDriverAvailability().ok ? ACP_CLI_KINDS : new Set();
28
+ } catch {
29
+ return new Set();
30
+ }
31
+ }
32
+
33
+ // 라이브 집합: 네이티브 4종 + (코어가 ACP 러너를 갖고 있으면) ACP 3종.
34
+ const CLI_EXECUTABLE_KINDS = new Proxy(NATIVE_CLI_KINDS, {
35
+ get(target, prop) {
36
+ const live = new Set([...target, ...acpKindsAvailable()]);
37
+ const value = live[prop];
38
+ return typeof value === "function" ? value.bind(live) : value;
39
+ },
40
+ });
41
+ const EXECUTABLE_KINDS = new Proxy(NATIVE_CLI_KINDS, {
42
+ get(target, prop) {
43
+ const live = new Set([...target, ...acpKindsAvailable(), ...API_EXECUTABLE_KINDS]);
44
+ const value = live[prop];
45
+ return typeof value === "function" ? value.bind(live) : value;
46
+ },
47
+ });
20
48
 
21
49
  function apiRuntime(kind, model, source) {
22
50
  if (!API_EXECUTABLE_KINDS.has(kind)) return null;
@@ -56,7 +84,10 @@ function resolveRuntime({ db, prefs, explicit }) {
56
84
  const bin = RUNTIME_BIN[explicit];
57
85
  if (!bin) throw new NoRuntimeError(`unknown runtime: ${explicit}`);
58
86
  if (!CLI_EXECUTABLE_KINDS.has(explicit)) {
59
- throw new NoRuntimeError(`runtime '${explicit}' has no v2 streaming driver yet (available: ${[...EXECUTABLE_KINDS].join(", ")})`);
87
+ const acpHint = ACP_CLI_KINDS.has(explicit)
88
+ ? ` — its ACP driver needs the desktop core with electron/runtime/acp.js (npm run vendor:core / agentlas doctor)`
89
+ : "";
90
+ throw new NoRuntimeError(`runtime '${explicit}' has no v2 streaming driver yet (available: ${[...EXECUTABLE_KINDS].join(", ")})${acpHint}`);
60
91
  }
61
92
  const p = whichSync(bin);
62
93
  if (!p) throw new NoRuntimeError(`runtime '${explicit}' requested but '${bin}' is not on PATH`);
@@ -109,6 +140,8 @@ module.exports = {
109
140
  NoRuntimeError,
110
141
  EXECUTABLE_KINDS,
111
142
  CLI_EXECUTABLE_KINDS,
143
+ NATIVE_CLI_KINDS,
144
+ ACP_CLI_KINDS,
112
145
  API_EXECUTABLE_KINDS,
113
146
  sharedRuntimeKind,
114
147
  };
@@ -107,6 +107,8 @@ const CATALOG = [
107
107
  { name: "import", group: "advanced", tier: "more", surfaces: BOTH, args: "<path>", argsKo: "<경로>", ko: "로컬 폴더 에이전트 가져오기", en: "Import a local folder agent" },
108
108
  { name: "cd", group: "advanced", tier: "more", surfaces: BOTH, args: "<agent>", argsKo: "<에이전트>", ko: "그 에이전트의 폴더 경로를 출력", en: "Print that agent's folder path" },
109
109
  { name: "native", group: "advanced", tier: "more", surfaces: BOTH, args: "prepare <agent>", argsKo: "prepare <에이전트>", ko: "네이티브 CLI 컨텍스트 생성", en: "Prepare native CLI context" },
110
+ // CLI only: stdout is the protocol wire, so it cannot run inside the REPL.
111
+ { name: "acp", group: "advanced", tier: "more", surfaces: CLI, args: "[--info]", ko: "에디터(Zed·JetBrains)용 ACP 에이전트로 실행", en: "Serve Agentlas as an ACP agent for editors (Zed, JetBrains)" },
110
112
  { name: "mcp", group: "advanced", tier: "more", surfaces: BOTH, args: "[list|probe <id>]", ko: "MCP 서버", en: "MCP servers" },
111
113
  { name: "plugin", group: "advanced", tier: "more", surfaces: BOTH, args: "<add <slug>|list|remove>", ko: "Hub 플러그인 (MCP 서버)", en: "Hub plugins (MCP servers)" },
112
114
  { name: "creds", group: "advanced", tier: "more", surfaces: BOTH, args: "<list|save|file>", ko: "API 키 보관 (값은 절대 표시 안 함)", en: "API keys (values are never printed)" },
@@ -28,7 +28,8 @@ const SLASH_COMMANDS = catalog.forSurface("repl").map((entry) => ({
28
28
  }));
29
29
 
30
30
  const SLASH_NAMES = SLASH_COMMANDS.map((c) => c.command);
31
- const RUNTIME_KINDS = ["claude-code", "codex", "agy", "gemini"];
31
+ // /runtime 완성 후보 정본(runtimes/kinds.cjs)의 네이티브 스폰 러너 4종.
32
+ const RUNTIME_KINDS = require("../runtimes/kinds.cjs").NATIVE_CLI_KINDS;
32
33
  const EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
33
34
  const PERM_LEVELS = ["read", "write", "full"];
34
35
  // 세션 인자를 받는 명령 — 완성 후보를 살아있는 세션 키(s1, s2…)로 채운다.
@@ -791,7 +791,8 @@ function handleSlash(ctx, cmdline, api) {
791
791
  * (REPL의 평문 입력이 곧 run이다).
792
792
  */
793
793
  // help/agents/list/mcp/doctor 등은 위 케이스에서 이미 처리된다.
794
- const REPL_EXCLUDED = new Set(["firm", "setup", "run"]);
794
+ // acp: stdout becomes the protocol wire — meaningless (and destructive) inside the REPL.
795
+ const REPL_EXCLUDED = new Set(["firm", "setup", "run", "acp"]);
795
796
  if (!REPL_EXCLUDED.has(cmd) && commands.COMMANDS[cmd]) {
796
797
  const result = commands.COMMANDS[cmd]().run(ctx, rest);
797
798
  if (result && typeof result.then === "function") {
@@ -45,6 +45,35 @@ function loadRenderer() {
45
45
  }
46
46
  }
47
47
 
48
+ /*
49
+ * 제어 블록 스트리퍼 — 정본은 벤더 코어의 shared/agent-control-blocks
50
+ * (Desktop·Mobile 과 같은 규칙: Memory Events/Delegate/Automation 헤딩,
51
+ * <<agentlas-ask>>·<<agentlas-surface>>·<<agentlas-one-followups>>·goal-complete
52
+ * 마커, 스트리밍 미완성 꼬리까지). 손 regex 는 Memory Events 만 알아 나머지
53
+ * 마커가 화면에 원문으로 샜다. 옛 벤더 번들이라 정본이 없으면 종전 regex 로
54
+ * fail-open — 스트리퍼 부재가 TUI 를 죽여선 안 된다.
55
+ */
56
+ let _stripCanonical; // undefined=미시도 · null=정본 없음 · function=정본
57
+ function stripControlBlocksForDisplay(text, streaming) {
58
+ if (_stripCanonical === undefined) {
59
+ try {
60
+ const loaded = require("../core/desktop-core.cjs").loadCoreShared("agent-control-blocks");
61
+ _stripCanonical = loaded && loaded.module && typeof loaded.module.stripAgentControlBlocks === "function"
62
+ ? loaded.module.stripAgentControlBlocks
63
+ : null;
64
+ } catch {
65
+ _stripCanonical = null;
66
+ }
67
+ }
68
+ const value = String(text);
69
+ if (_stripCanonical) {
70
+ try {
71
+ return _stripCanonical(value, { streaming: !!streaming });
72
+ } catch { /* 정본 실패 → 아래 종전 regex 로 fail-open */ }
73
+ }
74
+ return value.replace(/\n#{1,3} Memory Events\b[\s\S]*$/, "\n");
75
+ }
76
+
48
77
  /* Ui 를 상속해 write 초크포인트만 렌더러로 돌린다. */
49
78
  class ShellUi extends Ui {
50
79
  /*
@@ -127,16 +156,22 @@ class ShellUi extends Ui {
127
156
  if (!this._md) this.streamStart();
128
157
  this._mdText += String(text);
129
158
  /*
130
- * Memory Events 봉투는 런타임 계약(펜스 파이프라인이 수확)이지 사용자용이 아니다.
159
+ * 제어 블록은 런타임 계약(펜스 파이프라인이 수확)이지 사용자용이 아니다.
131
160
  * append-only 기본 REPL은 이미 찍힌 봉투를 지울 수 없지만, 누적 재렌더는
132
161
  * 표시만 잘라낼 수 있다 — 수확 경로(st.text/fences)는 건드리지 않는다.
162
+ * 정본 스트리퍼의 streaming 모드가 미완성 마커 꼬리도 한 프레임 감춘다.
133
163
  */
134
- const visible = this._mdText.replace(/\n#{1,3} Memory Events\b[\s\S]*$/, "\n");
164
+ const visible = stripControlBlocksForDisplay(this._mdText, true);
135
165
  this._md.setText(visible);
136
166
  this._tui.requestRender();
137
167
  this._streaming = true;
138
168
  }
139
169
  streamEnd() {
170
+ // 확정 렌더 — streaming 모드가 감춰 두던 꼬리를 settled 규칙으로 최종 판정한다.
171
+ if (this._md && this._mdText) {
172
+ this._md.setText(stripControlBlocksForDisplay(this._mdText, false));
173
+ this._tui.requestRender();
174
+ }
140
175
  this._md = null;
141
176
  this._mdText = "";
142
177
  this._streaming = false;
@@ -615,4 +650,4 @@ async function startShell(ctx, opts = {}) {
615
650
  return new Promise(() => {});
616
651
  }
617
652
 
618
- module.exports = { startShell };
653
+ module.exports = { startShell, stripControlBlocksForDisplay };
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "2",
3
- "url": "https://github.com/agentlas-ai/agentlas-terminal/releases/download/desktop-core-v2/desktop-core.tar.gz",
4
- "sha256": "782760c34af1d06b9efd212d48ba032fe46907bdc8bc836025c44fc658968d6f",
5
- "sizeBytes": 12295321,
6
- "writtenAt": "2026-08-09T21:43:34.707Z"
2
+ "version": "3",
3
+ "url": "https://github.com/agentlas-ai/agentlas-terminal/releases/download/desktop-core-v3/desktop-core.tar.gz",
4
+ "sha256": "a72cce0ccfe718e2a9255b1b4722d330ef9887e22be92c654a0435f17f5ee178",
5
+ "sizeBytes": 12437404,
6
+ "writtenAt": "2026-08-18T04:09:04.540Z"
7
7
  }
@@ -24,14 +24,10 @@ const path = require("node:path");
24
24
  const { spawn } = require("node:child_process");
25
25
  const { dbPath, userDataDir } = require("../core/paths.cjs");
26
26
 
27
- // 캡처 드라이버가 검증된 런타임만. v2 detect.cjs의 RUNTIME_BIN에는 kimi/grok/cursor도
28
- // 있지만 buildArgs/텍스트 추출 계약이 없으므로 여기 목록에 절대 조용히 추가하지 않는다.
29
- const RUNTIME_BIN = {
30
- "claude-code": "claude",
31
- codex: "codex",
32
- agy: "agy",
33
- gemini: "gemini",
34
- };
27
+ // 캡처 드라이버가 검증된 런타임만. 정본(runtimes/kinds.cjs)의 RUNTIME_BIN에는 kimi/grok/cursor도
28
+ // 있지만 buildArgs/텍스트 추출 계약이 없으므로 캡처 검증 파생본만 쓴다 새 kind 를
29
+ // 정본에 추가해도 capture:true 를 명시하기 전엔 여기 조용히 들어오지 않는다.
30
+ const { CAPTURE_RUNTIME_BIN: RUNTIME_BIN } = require("../runtimes/kinds.cjs");
35
31
 
36
32
  const SERVICE = "com.agentlas.desktop";
37
33
 
@@ -523,6 +523,13 @@ function projectContextSlice(projectPath, task) {
523
523
  "--task-stdin",
524
524
  "--no-refresh",
525
525
  "--render",
526
+ // Recall degrades to a labelled map, never to nothing. Core's passive
527
+ // freshness check walks the whole repository (measured 11.0s on the
528
+ // pilot) against this 4s timeout, and any non-zero exit is swallowed
529
+ // into "" below — so without a budget a large project silently lost
530
+ // its slice on every turn.
531
+ "--allow-stale",
532
+ "--freshness-budget", "0.4",
526
533
  ],
527
534
  {
528
535
  cwd: projectPath,
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.47",
3
+ "version": "1.0.49",
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"
7
7
  },
8
8
  "scripts": {
9
+ "test": "sh test/smoke.sh",
9
10
  "smoke": "sh test/smoke.sh",
10
11
  "test:release-contracts": "npm run smoke",
11
12
  "sync:architecture": "node scripts/sync-architecture-from-desktop.cjs",