agentlas 1.0.48 → 1.0.50

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.
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /*
3
+ * core/store-schema — 공유 저장소의 **스키마 버전 계약**. 터미널은 읽기만 한다.
4
+ *
5
+ * ★단일 마이그레이션 권위 (Phase 0, docs/DAEMON-ARCHITECTURE-DESIGN-2026-08-18.md §2/§6).
6
+ *
7
+ * `~/Library/Application Support/Agentlas/agentlas.sqlite` 는 락 없는 다중 쓰기 파일인데
8
+ * 마이그레이션 주인이 **둘**이었다:
9
+ * 1) 데스크탑의 사다리(electron/store/db.ts SCHEMA_VERSION),
10
+ * 2) 그 사다리를 그대로 다시 도는 터미널의 벤더 코어 경로(core/desktop-core.cjs initStore).
11
+ * 그리고 터미널의 가벼운 드라이버(core/db.cjs)는 user_version 을 **읽지도 않으면서** 쓰기
12
+ * 트랜잭션(seedBuiltins → BEGIN IMMEDIATE)을 열었다. 즉 "검사 없음 + 거절 없음 + 쓰기 있음".
13
+ *
14
+ * 이제 규칙은 하나다: **터미널은 절대 승급하지 않는다.** 파일이 이 배포가 아는 버전보다
15
+ * 낮으면 조용히 진행하지도, 몰래 마이그레이션하지도 않고 **정직하게 거절**한다.
16
+ *
17
+ * 왜 "없으면 터미널이 주인" 이 아닌가: 데스크탑의 부재는 경합 없이 관측할 수 없다 —
18
+ * 터미널이 사다리를 도는 도중에 데스크탑이 켜질 수 있다. 잘못 추측한 비용은 117MB 저장소
19
+ * 손상(db.ts 의 run_events 사고 주석)이고, 거절의 비용은 데스크탑 한 번 실행이다.
20
+ *
21
+ * 기대 버전의 출처: 이 패키지가 함께 배포하는 engine/bootstrap-schema.sql 의
22
+ * `PRAGMA user_version=` 이 곧 "이 배포가 아는 사다리 머리"다. 그 파일은 데스크탑 사다리를
23
+ * 빈 DB 에 끝까지 돌려 생성한 것이므로(scripts/gen-bootstrap-schema.cjs) 손으로 맞출 숫자가
24
+ * 따로 없다 — 재생성하면 자동으로 같이 움직인다.
25
+ */
26
+ const fs = require("node:fs");
27
+ const path = require("node:path");
28
+
29
+ const BOOTSTRAP_SCHEMA_FILE = path.join(path.dirname(__dirname), "bootstrap-schema.sql");
30
+
31
+ let _expected;
32
+
33
+ /** 이 배포가 아는 스키마 버전. 부트스트랩 SQL 헤더에서 읽는다(정본은 데스크탑 사다리). */
34
+ function expectedStoreSchemaVersion() {
35
+ if (_expected !== undefined) return _expected;
36
+ let header = "";
37
+ try {
38
+ const fd = fs.openSync(BOOTSTRAP_SCHEMA_FILE, "r");
39
+ try {
40
+ const buf = Buffer.alloc(4096);
41
+ const read = fs.readSync(fd, buf, 0, buf.length, 0);
42
+ header = buf.slice(0, read).toString("utf8");
43
+ } finally {
44
+ fs.closeSync(fd);
45
+ }
46
+ } catch {
47
+ header = "";
48
+ }
49
+ const match = /PRAGMA\s+user_version\s*=\s*(\d+)/i.exec(header);
50
+ // 부트스트랩 SQL 을 못 읽으면 기대 버전을 **모른다**. 0 은 "검사 불가"를 뜻하고,
51
+ // 아래 단언은 그때 통과시킨다 — 모르는 것을 근거로 사용자를 막지 않는다.
52
+ _expected = match ? Number(match[1]) : 0;
53
+ return _expected;
54
+ }
55
+
56
+ /** 열린 커넥션의 user_version. 못 읽으면 null(모름). */
57
+ function readStoreSchemaVersion(db) {
58
+ try {
59
+ if (typeof db.pragma === "function") {
60
+ const value = db.pragma("user_version", { simple: true });
61
+ return Number.isFinite(Number(value)) ? Number(value) : null;
62
+ }
63
+ const row = db.prepare("PRAGMA user_version").get();
64
+ if (!row) return null;
65
+ const value = row.user_version ?? Object.values(row)[0];
66
+ return Number.isFinite(Number(value)) ? Number(value) : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /** 정직하고 실행 가능한 거절문. 두 가지 해결책을 모두 이름으로 말한다. */
73
+ function storeSchemaRefusalMessage(found, expected, file) {
74
+ return [
75
+ `Agentlas store schema is v${found}, but this Agentlas CLI needs v${expected}.`,
76
+ `Store: ${file}`,
77
+ "The Agentlas CLI never migrates the shared database — the Desktop app owns the migration ladder,",
78
+ "and a second migrator on this lock-free file is how the store was corrupted before.",
79
+ "",
80
+ "Fix it once, either way:",
81
+ " • Launch (or update) the Agentlas Desktop app once, then re-run this command.",
82
+ " • No Desktop app on this machine? Close every Agentlas process, then run this command once with",
83
+ " AGENTLAS_STORE_MIGRATION_ROLE=owner set, so the upgrade is a deliberate act rather than a race.",
84
+ ].join("\n");
85
+ }
86
+
87
+ class StoreSchemaTooOldError extends Error {
88
+ constructor(found, expected, file) {
89
+ super(storeSchemaRefusalMessage(found, expected, file));
90
+ this.name = "StoreSchemaTooOldError";
91
+ this.code = "AGENTLAS_STORE_SCHEMA_TOO_OLD";
92
+ this.found = found;
93
+ this.expected = expected;
94
+ this.file = file;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 공유 저장소를 이 프로세스가 써도 되는가.
100
+ * 낮으면 던진다. 같거나 높으면 통과 — 더 높은 것은 데스크탑이 앞서간 것이고, 터미널은
101
+ * columnExists 기반 방어적 읽기로 전진 호환한다(기존 계약 유지).
102
+ */
103
+ function assertStoreSchemaCompatible(db, file) {
104
+ const expected = expectedStoreSchemaVersion();
105
+ if (!expected) return; // 기대 버전을 모르면 막지 않는다.
106
+ const found = readStoreSchemaVersion(db);
107
+ if (found === null) return; // 읽을 수 없으면 판단하지 않는다(가짜 거절 금지).
108
+ if (found >= expected) return;
109
+ throw new StoreSchemaTooOldError(found, expected, file);
110
+ }
111
+
112
+ module.exports = {
113
+ BOOTSTRAP_SCHEMA_FILE,
114
+ StoreSchemaTooOldError,
115
+ assertStoreSchemaCompatible,
116
+ expectedStoreSchemaVersion,
117
+ readStoreSchemaVersion,
118
+ storeSchemaRefusalMessage,
119
+ };
@@ -184,6 +184,20 @@ const RULES = [
184
184
  " the fact (kind:\"code\" or a read step with uses) into its own produces, then set the",
185
185
  " check's evidence:\"<that name>\". The check then compares result against evidence.",
186
186
  " Only ask when the goal itself is too vague to know what the result even is.",
187
+ " · MUTATION RESULT — verify it landed, not that the model said so (REQUIRED for every",
188
+ " effect:\"mutation\" step that acts on the outside world — posting, sending, saving,",
189
+ " updating). A run that writes \"posted 3 replies\" has not posted anything; only an",
190
+ " INDEPENDENT observation of the outside world proves it. So the mutation step must",
191
+ " produce the identifiers needed to observe its result (the posted URLs/ids, the file",
192
+ " path, the updated row id) — put that in its produces. Then add, AFTER it, a read step",
193
+ " (effect:\"read\") that RE-OBSERVES the result from the outside — a fresh look, not the",
194
+ " mutation's own report: open each returned URL, re-read the saved file, re-fetch the",
195
+ " updated row — into its own produces. Then add a check whose subject is the mutation's",
196
+ " result and whose evidence is that observation, with must-items like 'each posted item",
197
+ " exists at its URL with the expected author and text' / 'the file exists and contains",
198
+ " the expected fields' / 'the row was appended with the submitted values'. If the outside",
199
+ " result genuinely cannot be re-observed, say so in the step instruction rather than",
200
+ " skipping the check.",
187
201
  " · repeatOn says which side loops. Write the condition the way the person said it and",
188
202
  " put the loop on the side they meant — do not flip either one to make it fit.",
189
203
  "",
@@ -480,6 +494,57 @@ function validateBlueprint(bp, ctx = {}) {
480
494
  });
481
495
  }
482
496
 
497
+ /*
498
+ * ★바깥을 바꾼 mutation의 **결과**는 독립 재조회로 확인해야 한다(입력이 아니라 결과).
499
+ *
500
+ * 위 블록은 mutation이 **소비하는 입력값**을 검증하게 한다(게시할 목록이 채워졌나).
501
+ * 하지만 "게시가 실제로 됐나"는 그것으로 답이 안 된다 — 모델이 "게시 완료"라고 써도
502
+ * 바깥에는 아무것도 없을 수 있다(실측 2026-08-19: X 자동화가 두 런타임에서 4/4로 끝나며
503
+ * "3건 게시"라고 적었지만 X엔 0건). 결과가 실제로 반영됐는지는 **바깥을 다시 관측한
504
+ * 근거**로만 판정된다.
505
+ *
506
+ * 데스크탑 정본(shared/graph-blueprint.ts)과 같은 규칙 — 플러그인(hep-graph)으로 만든
507
+ * 자동화도 같은 보장을 받아야 한다. 두 입구가 다른 그래프를 만들면 안 된다.
508
+ */
509
+ {
510
+ const resultChecks = new Map();
511
+ for (const check of bp.checks || []) {
512
+ const subj = (check.subject || "").trim();
513
+ if (subj) resultChecks.set(subj, check);
514
+ }
515
+ steps.forEach((step, index) => {
516
+ if (step.effect !== "mutation") return;
517
+ const result = String(step.produces || "").trim();
518
+ if (!result) {
519
+ push(
520
+ `"${step.title || `${index + 1}번째 단계`}"는 바깥을 바꾸는데 결과값(produces)이 없습니다. `
521
+ + `무엇이 반영됐는지(게시된 URL·저장된 경로·갱신된 행 id 등)를 produces로 내보내야 `
522
+ + `그 결과가 실제로 일어났는지 확인할 수 있습니다.`,
523
+ );
524
+ return;
525
+ }
526
+ const check = resultChecks.get(result);
527
+ const hasEvidence = !!check && !!String((check && check.evidence) || "").trim();
528
+ if (!hasEvidence) {
529
+ const madeBy = steps.findIndex(
530
+ (s, i) => i > index && s.effect === "read"
531
+ && (Array.isArray(s.consumes) ? s.consumes : []).map((v) => String(v).trim()).includes(result),
532
+ );
533
+ const evName = `${result}_observed`;
534
+ push(
535
+ `"${step.title || `${index + 1}번째 단계`}"는 바깥을 바꿨지만, 그 결과가 실제로 반영됐는지 `
536
+ + `**독립적으로 다시 관측해** 확인하는 검증이 없습니다. 모델이 "완료"라고 써도 바깥은 그대로일 수 `
537
+ + `있습니다. 단계를 지우지 말고: (1) 이 단계 뒤에 결과를 바깥에서 다시 보는 read 단계`
538
+ + `(게시 URL 열기·파일 다시 읽기·행 재조회)를 두어 그 관측을 "${evName}"으로 내보내고, `
539
+ + `(2) top-level checks[]에 {"afterStep":${madeBy >= 0 ? madeBy : index + 1},"subject":"${result}",`
540
+ + `"criteria":"${result}이(가) 바깥에 실제로 반영됐다","evidence":"${evName}",`
541
+ + `"produces":"${result}_ok","items":[{"text":"관측된 결과가 반영하려던 것과 일치한다","kind":"must"},`
542
+ + `{"text":"관측되지 않았거나 지어낸 확인이 아니다","kind":"mustNot"}]} 를 추가하세요.`,
543
+ );
544
+ }
545
+ });
546
+ }
547
+
483
548
  // 반복이 있는데 검증이 없으면 "마음에 들 때까지"를 글자 찾기로 흉내 내게 된다(실측).
484
549
  for (const branch of bp.branches || []) {
485
550
  if (branch.repeatStep === undefined) continue;
@@ -634,7 +699,17 @@ function humanSchedule(schedule, locale) {
634
699
 
635
700
  function hhmm(hour, minute, locale) {
636
701
  if (locale !== "ko") return `${hour}:${minute}`;
637
- return `${hour}:${minute}`;
702
+ /*
703
+ * ★한국어는 사람이 말하는 대로 — "오전 8시", "오후 6시 30분" (오너 결정 2026-08-19).
704
+ * 정본은 데스크탑 shared/schedule-describe.ts 의 koClock 이고, 이 파일은 그 손복사본이다
705
+ * (test/graph-interview-parity.cjs 가 두 벌이 갈리는 순간 실패한다).
706
+ */
707
+ const h = Number(hour);
708
+ const m = Number(minute);
709
+ if (!Number.isFinite(h) || !Number.isFinite(m)) return `${hour}:${minute}`;
710
+ const half = h < 12 ? "오전" : "오후";
711
+ const display = h % 12 === 0 ? 12 : h % 12;
712
+ return m === 0 ? `${half} ${display}시` : `${half} ${display}시 ${m}분`;
638
713
  }
639
714
 
640
715
  const DOW_KO = { "0": "일", "1": "월", "2": "화", "3": "수", "4": "목", "5": "금", "6": "토", "7": "일" };
@@ -7,7 +7,7 @@
7
7
  "use strict";
8
8
 
9
9
  const GRAPH_WIRE = "graph/1";
10
- const GRAPH_ERROR_CODES = ["APPROVAL_REQUIRED","APPROVAL_TIMED_OUT","ARCHITECT_NO_CHANGE","ARCHITECT_NO_REQUEST","ARCHITECT_OUTPUT_MALFORMED","ARCHITECT_OUTPUT_TOO_LARGE","ARCHITECT_OUTPUT_UNREADABLE","ARCHITECT_UNAVAILABLE","AUTOMATION_NOT_CONNECTED","BUDGET_EXHAUSTED","CODE_DEPENDENCY_MISSING","CODE_NODE_EMPTY","CODE_PRODUCED_NOTHING","CODE_STEP_FAILED","CREATE_INPUT_INVALID","EDGE_CONDITION_UNRESOLVED","EVAL_INCOMPLETE","EVAL_STUCK","EVAL_UNAVAILABLE","INTERVIEW_MODEL_UNAVAILABLE","INTERVIEW_OUTPUT_UNREADABLE","INTERVIEW_REPEATED_QUESTIONS","INTERVIEW_SELF_CORRECTION_EXHAUSTED","INTERVIEW_STATE_INVALID","LOOP_BOUND_INVALID","LOOP_BOUND_UNDECLARED","LOOP_LIMIT_REACHED","LOOP_WITHOUT_EXIT","MUTATION_UNVERIFIED","NODE_FAILED","NODE_INPUT_MISSING","NODE_NEVER_REACHED","NODE_NO_RESULT","NODE_TIMEOUT","NODE_TYPE_UNSUPPORTED","NO_MATCHING_EDGE","OUTPUT_NODE_EMPTY","PATCH_CODE_EMPTY","PATCH_EDGE_CONFLICT","PATCH_EDGE_DANGLING","PATCH_EDGE_HANDLE_MISSING","PATCH_EDGE_MISSING","PATCH_EMPTY","PATCH_LOOP_BOUND_MISSING","PATCH_NODE_CONFLICT","PATCH_NODE_MISSING","PATCH_NO_GRAPH","PATCH_OP_UNKNOWN","REDUCER_MERGE_CONFLICT","REDUCER_WRITE_CONFLICT","RESUME_CONFLICT","RUN_REQUEST_DISABLED","RUN_REQUEST_INPUT_REQUIRED","RUN_REQUEST_NOT_FOUND","RUN_REQUEST_QUEUE_UNAVAILABLE","RUN_REQUEST_REF_AMBIGUOUS","RUN_REQUEST_REF_MISSING","SUBGRAPH_DEPTH_EXCEEDED","SUBGRAPH_FAILED","SUBGRAPH_NOT_FOUND","SUBGRAPH_NO_RESULT","SUBGRAPH_SELF_CALL","SWAP_CAPABILITY_MISMATCH","SWAP_HUB_RELEASE_UNPINNED","SWAP_NODE_NOT_FOUND","SWAP_NOT_AGENT_NODE","SWAP_NO_MATCH","SWAP_UNKNOWN_PROVIDER","TOOL_BROKER_CALL_UNREADABLE","TOOL_BROKER_MUTATION_IN_SIMULATION","TOOL_BROKER_PLAN_UNREADABLE","TOOL_BROKER_TOOL_NOT_DECLARED","TOOL_NODE_UNATTACHED","TOOL_NODE_UNCONFIGURED","TRANSFORM_MODE_UNKNOWN","TRANSFORM_NODE_UNCONFIGURED"];
10
+ const GRAPH_ERROR_CODES = ["APPROVAL_REQUIRED","APPROVAL_TIMED_OUT","ARCHITECT_NO_CHANGE","ARCHITECT_NO_REQUEST","ARCHITECT_OUTPUT_MALFORMED","ARCHITECT_OUTPUT_TOO_LARGE","ARCHITECT_OUTPUT_UNREADABLE","ARCHITECT_UNAVAILABLE","AUTOMATION_NOT_CONNECTED","BUDGET_EXHAUSTED","CODE_DEPENDENCY_MISSING","CODE_NODE_EMPTY","CODE_PRODUCED_NOTHING","CODE_STEP_FAILED","CREATE_INPUT_INVALID","EDGE_CONDITION_UNRESOLVED","EVAL_FAILED","EVAL_INCOMPLETE","EVAL_STUCK","EVAL_UNAVAILABLE","INTERVIEW_MODEL_UNAVAILABLE","INTERVIEW_OUTPUT_UNREADABLE","INTERVIEW_REPEATED_QUESTIONS","INTERVIEW_SELF_CORRECTION_EXHAUSTED","INTERVIEW_STATE_INVALID","LOOP_BOUND_INVALID","LOOP_BOUND_UNDECLARED","LOOP_LIMIT_REACHED","LOOP_WITHOUT_EXIT","MUTATION_UNVERIFIED","NODE_CLAIMED_WITHOUT_TOOLS","NODE_FAILED","NODE_INPUT_MISSING","NODE_NEVER_REACHED","NODE_NO_RESULT","NODE_TIMEOUT","NODE_TYPE_UNSUPPORTED","NO_MATCHING_EDGE","OUTPUT_NODE_EMPTY","PATCH_CODE_EMPTY","PATCH_EDGE_CONFLICT","PATCH_EDGE_DANGLING","PATCH_EDGE_HANDLE_MISSING","PATCH_EDGE_MISSING","PATCH_EMPTY","PATCH_LOOP_BOUND_MISSING","PATCH_NODE_CONFLICT","PATCH_NODE_MISSING","PATCH_NO_GRAPH","PATCH_OP_UNKNOWN","REDUCER_MERGE_CONFLICT","REDUCER_WRITE_CONFLICT","RESUME_CONFLICT","RUN_REQUEST_DISABLED","RUN_REQUEST_INPUT_REQUIRED","RUN_REQUEST_NOT_FOUND","RUN_REQUEST_QUEUE_UNAVAILABLE","RUN_REQUEST_REF_AMBIGUOUS","RUN_REQUEST_REF_MISSING","SUBGRAPH_DEPTH_EXCEEDED","SUBGRAPH_FAILED","SUBGRAPH_NOT_FOUND","SUBGRAPH_NO_RESULT","SUBGRAPH_SELF_CALL","SWAP_CAPABILITY_MISMATCH","SWAP_HUB_RELEASE_UNPINNED","SWAP_NODE_NOT_FOUND","SWAP_NOT_AGENT_NODE","SWAP_NO_MATCH","SWAP_UNKNOWN_PROVIDER","TOOL_BROKER_CALL_UNREADABLE","TOOL_BROKER_MUTATION_IN_SIMULATION","TOOL_BROKER_PLAN_UNREADABLE","TOOL_BROKER_TOOL_NOT_DECLARED","TOOL_NODE_UNATTACHED","TOOL_NODE_UNCONFIGURED","TRANSFORM_MODE_UNKNOWN","TRANSFORM_NODE_UNCONFIGURED"];
11
11
  const GRAPH_JOURNAL_KINDS = ["blob_externalized","node_failed","node_intent","node_reserved","node_retry","node_routed","node_settled","resumed","run_completed","run_created","run_failed","run_validated","suspended"];
12
12
  const GRAPH_NODE_KINDS = ["action","agent","code","condition","eval","output","subgraph","tool","transform","trigger"];
13
13
  const GRAPH_BLOCK_UI = {"trigger":{"section":"none","placeable":false,"placeReason":"그래프마다 하나뿐이고 처음 만들 때 함께 지어진다"},"agent":{"section":"inventory","placeable":true},"eval":{"section":"flow","placeable":true},"condition":{"section":"flow","placeable":true},"transform":{"section":"flow","placeable":true},"code":{"section":"flow","placeable":true},"tool":{"section":"inventory","placeable":true},"action":{"section":"actions","placeable":true},"output":{"section":"flow","placeable":true},"loop":{"section":"none","placeable":false,"placeReason":"노드가 아니라 되돌아가는 연결의 성질이다 — 엣지를 이어서 만든다"},"subgraph":{"section":"flow","placeable":true}};
@@ -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,
@@ -30,6 +30,43 @@ const RUNTIME_KIND_SPECS = [
30
30
  /** kind → 실행 파일 이름. CLI 런타임 전체(네이티브 + ACP). */
31
31
  const RUNTIME_BIN = Object.fromEntries(RUNTIME_KIND_SPECS.map((s) => [s.kind, s.bin]));
32
32
 
33
+ /**
34
+ * 데스크탑이 **같은 DB 에 적어 둔 이름** → 이 저장소의 이름.
35
+ *
36
+ * ★터미널과 데스크탑은 하나의 SQLite 를 공유하는데 같은 런타임을 다르게 부른다:
37
+ * 데스크탑 `shared/runtime-kinds.ts` 는 `antigravity`, 여기는 `agy` 다. 그 차이는
38
+ * 주석에만 적혀 있었고 **읽는 자리에서 번역되지 않았다**.
39
+ *
40
+ * 결과(실측 2026-08-19): 사용자가 오케스트레이터를 Antigravity 로 골라 두면
41
+ * `model_roles.kind = "antigravity"` 가 저장되는데, 터미널의 실행 가능 집합에는
42
+ * 그 이름이 없어 **"이 컴퓨터에서 실행 불가"로 걸러지고** 풀 3순위(codex)가 대신 돌았다.
43
+ * `agentlas roles` 는 antigravity 라고 보여 주면서 그래프 빌더는 codex 로 지었다 —
44
+ * 고른 대로 안 도는데 화면은 고른 대로 보였다.
45
+ *
46
+ * 저장된 이름은 못 바꾼다(데스크탑이 계속 그렇게 쓴다). 그러니 **읽는 쪽이 번역한다**.
47
+ */
48
+ const STORED_KIND_ALIASES = { antigravity: "agy" };
49
+
50
+ /** 저장소에서 읽은 kind 를 이 저장소의 정본 이름으로. 모르는 값은 그대로 돌려준다. */
51
+ function canonicalRuntimeKind(kind) {
52
+ const text = typeof kind === "string" ? kind.trim() : "";
53
+ if (!text) return text;
54
+ return STORED_KIND_ALIASES[text] ?? text;
55
+ }
56
+
57
+ /** 역방향 — 공유 DB 에 적을 이름. 어긋남은 **양쪽으로** 난다:
58
+ * `agentlas roles set orchestrator agy` 가 `agy` 를 그대로 적으면, 이번에는
59
+ * 데스크탑이 그 이름을 모른다(shared/runtime-kinds.ts 에 `agy` 가 없다).
60
+ * 저장 어휘는 스키마 주인인 데스크탑 쪽으로 통일하고, 읽을 때 위에서 되돌린다. */
61
+ const CANONICAL_TO_STORED = Object.fromEntries(
62
+ Object.entries(STORED_KIND_ALIASES).map(([stored, canonical]) => [canonical, stored]),
63
+ );
64
+ function storedRuntimeKind(kind) {
65
+ const text = typeof kind === "string" ? kind.trim() : "";
66
+ if (!text) return text;
67
+ return CANONICAL_TO_STORED[text] ?? text;
68
+ }
69
+
33
70
  /** CLI 런타임 kind 전체(탐지 순서). */
34
71
  const CLI_KINDS = RUNTIME_KIND_SPECS.map((s) => s.kind);
35
72
 
@@ -71,6 +108,9 @@ const CONTRACT_RUNTIME_BACKENDS = [
71
108
  module.exports = {
72
109
  RUNTIME_KIND_SPECS,
73
110
  RUNTIME_BIN,
111
+ STORED_KIND_ALIASES,
112
+ canonicalRuntimeKind,
113
+ storedRuntimeKind,
74
114
  CLI_KINDS,
75
115
  NATIVE_CLI_KINDS,
76
116
  ACP_CLI_KINDS,
@@ -20,6 +20,7 @@ const {
20
20
  API_EXECUTABLE_KINDS,
21
21
  } = require("./resolve.cjs");
22
22
  const { pickRoleFromPool } = require("./roles.cjs");
23
+ const { canonicalRuntimeKind } = require("./kinds.cjs");
23
24
 
24
25
  const OVERRIDE_TABLE = "agent_runtime_overrides";
25
26
  // 데스크탑 VALID_SCOPES 동형. v2 터미널 호출자는 주로 'agent'지만 firm/division도 읽을 수 있다.
@@ -38,7 +39,8 @@ function rowToOverride(row) {
38
39
  targetId: row.target_id,
39
40
  label: cleanText(row.label),
40
41
  selection: {
41
- kind: String(row.kind),
42
+ // 저장된 이름(데스크탑 표기) → 이 저장소의 이름. kinds.cjs 참고.
43
+ kind: canonicalRuntimeKind(String(row.kind)),
42
44
  backend: cleanText(row.backend) || undefined,
43
45
  source: cleanText(row.source) || undefined,
44
46
  model: cleanText(row.model) || undefined,
@@ -9,6 +9,7 @@
9
9
  * The worker may inherit upward for quality; the orchestrator never falls
10
10
  * downward to the worker row.
11
11
  */
12
+ const { canonicalRuntimeKind } = require("./kinds.cjs");
12
13
  const { tableExists, columnExists } = require("../core/db.cjs");
13
14
 
14
15
  const MODEL_ROLE_TABLE = "model_roles";
@@ -49,7 +50,7 @@ function legacyOrchestrator(db) {
49
50
  }
50
51
  return {
51
52
  role: "orchestrator",
52
- kind: cleanText(row.kind),
53
+ kind: canonicalRuntimeKind(cleanText(row.kind)),
53
54
  backend: cleanText(row.backend),
54
55
  source: cleanText(row.source),
55
56
  model: cleanText(row.model),
@@ -68,7 +69,8 @@ function normalizedRow(row, role) {
68
69
  if (!row || !cleanText(row.kind)) return null;
69
70
  return {
70
71
  role,
71
- kind: cleanText(row.kind),
72
+ // 저장된 이름(데스크탑 표기)을 이 저장소의 이름으로 번역한다 — kinds.cjs 참고.
73
+ kind: canonicalRuntimeKind(cleanText(row.kind)),
72
74
  backend: cleanText(row.backend),
73
75
  source: cleanText(row.source),
74
76
  model: cleanText(row.model),
@@ -28,17 +28,10 @@ const { dbPath, userDataDir } = require("../core/paths.cjs");
28
28
  // 있지만 buildArgs/텍스트 추출 계약이 없으므로 캡처 검증 파생본만 쓴다 — 새 kind 를
29
29
  // 정본에 추가해도 capture:true 를 명시하기 전엔 여기 조용히 들어오지 않는다.
30
30
  const { CAPTURE_RUNTIME_BIN: RUNTIME_BIN } = require("../runtimes/kinds.cjs");
31
+ const { readKeychainPassword } = require("../core/keychain-read.cjs");
31
32
 
32
33
  const SERVICE = "com.agentlas.desktop";
33
34
 
34
- function readKeytar() {
35
- try {
36
- return require("keytar");
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
35
  // v1 which 포팅: PATH + 알려진 설치 위치. detect.whichSync(spawn `which`)와 달리
43
36
  // 캡처 경로는 프로세스 spawn 없이 결정론적으로 실행 파일을 찾는다.
44
37
  function which(cmd) {
@@ -664,10 +657,11 @@ const ANTHROPIC_COMPAT_API = {
664
657
  const DEFAULT_CUSTOM_API_BASE_URL = "https://api.openai.com/v1";
665
658
 
666
659
  async function apiKey(backend) {
667
- const keytar = readKeytar();
668
- if (!keytar) return null;
669
- // 키체인 접근 거부(서명 standalone Node)는 "키 없음"으로 조용히 처리.
670
- return keytar.getPassword(SERVICE, "byok:" + backend).catch(() => null);
660
+ // ★거부가 아니라 **정지**가 실제 실패 모양이다. 예전에는 `.catch(() => null)` 하나로
661
+ // "접근 거부는 키 없음" 이라 적어 두었는데, 화면 없는 호스트에서 keytar 거부하지 않고
662
+ // 영영 돌아오지 않는다(이벤트 루프째로). core/keychain-read 상한을 실제로 있는
663
+ // 자식 프로세스에서 읽고, 읽으면 여기 계약대로 "키 없음"을 돌려준다.
664
+ return readKeychainPassword(SERVICE, "byok:" + backend);
671
665
  }
672
666
 
673
667
  // Custom BYOK 키가 전송될 origin 재검증: 공개 주소는 HTTPS만, HTTP는 localhost/LAN만.
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.48",
3
+ "version": "1.0.50",
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"