deel-local-cli 1.17.3 → 1.17.5

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,191 @@
1
+ /**
2
+ * 겪은 것을 **얼마나 믿을까.**
3
+ *
4
+ * ── 왜 따로 있나 ────────────────────────────────────────────────────────
5
+ *
6
+ * evolve.js 는 「무엇을 겪었나」 를 센다. 이 파일은 그 셈을 보고 「그것을 사실로
7
+ * 말해도 되나」 를 정한다. 두 가지는 자꾸 섞이는데, 섞어 두면 셈을 고칠 때마다
8
+ * 믿음의 문턱이 같이 흔들린다. 그리고 문턱이 흔들리는 것은 **화면에 안 나타난다.**
9
+ *
10
+ * ── 무엇이 문제였나 ─────────────────────────────────────────────────────
11
+ *
12
+ * 자기가 쓴 기록을 자기가 다시 읽는 프로그램은 두 가지로 썩는다.
13
+ *
14
+ * 1. **작은 표본을 확신으로 읽는다.** 두 번 실패한 것을 「이 PC 에서 안 되는
15
+ * 명령」 이라고 프롬프트에 적어 보냈다. 두 번은 우연일 수 있다.
16
+ *
17
+ * 2. **틀린 부정이 스스로를 봉인한다.** 「다시 부르지 마라」 가 프롬프트에
18
+ * 들어가면 모델이 안 부르고, 안 부르니 성공 셈이 영영 안 늘고, 그래서
19
+ * 그 줄이 영영 안 사라진다. 지난달에 없던 프로그램을 오늘 깔아도
20
+ * deel 은 없다고 우긴다. 고쳐질 길이 **구조적으로** 막혀 있었다.
21
+ *
22
+ * 둘 다 새로운 문제가 아니다. 남들이 이미 넘은 자리라 그 방식을 그대로 쓴다.
23
+ *
24
+ * ── 빌려 온 방식 ────────────────────────────────────────────────────────
25
+ *
26
+ * **윌슨 점수 아래끝 (Wilson score lower bound)**
27
+ * 별점 정렬에서 「5점 하나짜리」 가 「4.8점 천 개짜리」 를 이기던 문제를 푼
28
+ * 방식이다. 비율을 그대로 믿지 않고, 표본이 적으면 그만큼 **깎아서** 본다.
29
+ * 두 번 성공은 두 번어치만 믿는다. 근거: Evan Miller,
30
+ * "How Not To Sort By Average Rating" (evanmiller.org).
31
+ *
32
+ * **시간 할인 (discounted counts · Discounted-UCB)**
33
+ * 세상이 안 멈춰 있는 자리에서 옛 관측을 조금씩 잊는 방식이다. 관측마다
34
+ * `γ^(지난 날수)` 를 곱해 두면, 다시 안 겪은 사실은 저절로 힘이 빠진다.
35
+ * 근거: Garivier & Moulines, "On Upper-Confidence Bound Policies for
36
+ * Switching Bandit Problems" (Discounted-UCB · Sliding-Window UCB).
37
+ *
38
+ * **이 둘을 겹치면 2번이 저절로 풀린다.** 「안 된다」 는 다시 안 겪으면 힘이
39
+ * 빠지고, 힘이 빠지면 말하기를 멈추고, 그러면 모델이 다시 해 보고, 그 결과가
40
+ * 새 관측이 된다. 봉인을 따로 깨는 장치를 넣을 필요가 없다 — 잊는 것이 곧
41
+ * 다시 해 보는 것이다.
42
+ *
43
+ * ── 여기 없는 것 ────────────────────────────────────────────────────────
44
+ *
45
+ * 파일에서 읽은 것이 **애초에 우리가 적은 모양이 맞나**는 여기서 안 본다
46
+ * (agent/성한기록.js). 통계와 문지기는 다른 일이고, 섞어 두면 통계 문턱을
47
+ * 만질 때마다 문지기도 같이 흔들린다.
48
+ *
49
+ * ── 안 하는 것 ──────────────────────────────────────────────────────────
50
+ *
51
+ * **지우지 않는다.** 못 믿겠다는 것과 없던 일이라는 것은 다르다. 지워 버리면
52
+ * 다시 겪을 때 처음부터 두 번을 채워야 하고, 그 사이에 또 우회로를 탄다.
53
+ * 여기가 정하는 것은 「말할까」 하나뿐이고, 셈은 evolve.js 가 그대로 들고 있다.
54
+ */
55
+
56
+ /*
57
+ * 믿음의 폭. z=1 은 약 68% 구간이다.
58
+ *
59
+ * 95%(z≈1.96)로 조이면 열 번을 겪어야 겨우 말할 수 있게 되는데, 사람이 한
60
+ * 폴더에서 같은 명령을 열 번 돌리기 전에 대화가 끝난다. 그러면 이 기능이
61
+ * 켜져 있으나 마나가 된다 — 안 배우느니만 못한 것이 아니라, 그냥 안 배운다.
62
+ *
63
+ * z=1 은 「두 번 겪으면 말하고, 한 번은 말하지 않는다」 에 딱 맞는다. 여태
64
+ * 쓰던 규칙(`ok>=2 && no===0`)이 우연히 서 있던 자리이기도 하다. 다른 점은,
65
+ * 이제 그 자리가 **어디서 온 숫자인지** 적혀 있다는 것이다.
66
+ */
67
+ export const z = 1;
68
+
69
+ /** 이 위로 올라가야 「된다」 고 말한다 (성공률 아래끝). */
70
+ export const 좋음문턱 = 0.6;
71
+ /** 이 아래로 내려가야 「안 된다」 고 말한다 (성공률 위끝). */
72
+ export const 나쁨문턱 = 0.4;
73
+
74
+ /*
75
+ * ── 반감기가 둘인 까닭 ──────────────────────────────────────────────────
76
+ *
77
+ * 다시 안 겪은 관측이 절반으로 줄기까지의 날수인데, **「된다」 와 「안 된다」 에
78
+ * 다른 값을 준다.** 언뜻 자의적으로 보이지만 근거가 있다.
79
+ *
80
+ * 틀린 긍정("이 명령이 된다")은 한 번 해 보면 그 자리에서 들통난다. 값은
81
+ * 실패한 명령 한 번이고, 그 실패가 곧 새 관측이 되어 스스로를 고친다.
82
+ *
83
+ * 틀린 부정("안 된다")은 **해 보는 일 자체를 막는다.** 프롬프트가 부르지
84
+ * 말라고 하니 모델이 안 부르고, 안 부르니 관측이 안 생기고, 그래서 영영 안
85
+ * 고쳐진다. 들통날 기회를 자기가 없앤다.
86
+ *
87
+ * 그래서 부정은 더 빨리 잊는다. 반감기 사흘이면 **두 번 겪은 「안 된다」 는
88
+ * 하루쯤 뒤에** 입을 다문다. 그러면 모델이 다시 해 보고, 정말 안 되면 그날
89
+ * 다시 쌓인다 — 다섯 번 겪은 것은 닷새쯤 간다. 하루에 한 번 헛부름은
90
+ * 「한 달 내내 틀린 말을 프롬프트에 싣는 것」 보다 훨씬 싸다.
91
+ *
92
+ * 긍정은 세 주로 둔다. 이레로 뒀더니 **금요일에 확인한 명령이 월요일 아침에
93
+ * 백지가 됐다** — 그건 잊는 것이 아니라 안 배우는 것이다. 세 주면 두 번 겪은
94
+ * 사실이 아흐레쯤 가고, 다섯 번 겪은 사실은 한 달을 넘긴다.
95
+ */
96
+ export const 반감기된다 = 21;
97
+ export const 반감기안된다 = 3;
98
+
99
+ /** 하루에 곱해지는 값. 반감기에서 나온다 — 두 곳에 따로 적으면 언젠가 갈린다. */
100
+ export const 하루할인된다 = Math.pow(0.5, 1 / 반감기된다);
101
+ export const 하루할인안된다 = Math.pow(0.5, 1 / 반감기안된다);
102
+
103
+ /**
104
+ * 며칠 지났나. 못 재면 무한대다.
105
+ *
106
+ * **모르는 것을 「오늘」 로 치지 않는다.** 옛 판에서 넘어왔거나 손으로 고쳐진
107
+ * 기록에는 날짜가 없을 수 있는데, 그것을 오늘 겪은 것으로 치면 나이를 알 수
108
+ * 없는 주장이 통째로 되살아난다 — 이 파일이 막으려는 바로 그 오염이다.
109
+ *
110
+ * 미래에 적힌 것은 0일로 친다. 시계가 틀어진 PC 에서 음수 날수가 나오면
111
+ * 할인이 1보다 커져서 **겪지도 않은 것이 더 세진다.**
112
+ */
113
+ export function 며칠됐나(지금, 그때) {
114
+ const a = Date.parse(String(지금 ?? ''));
115
+ const b = Date.parse(String(그때 ?? ''));
116
+ if (!Number.isFinite(a) || !Number.isFinite(b)) return Infinity;
117
+ return Math.max(0, (a - b) / 86400000);
118
+ }
119
+
120
+ /**
121
+ * 지난 셈을 오늘 기준으로 삭힌다 (Discounted-UCB 의 할인).
122
+ *
123
+ * 관측 하나하나에 날짜를 달아 두지 않아도 된다. 마지막으로 손댄 날부터
124
+ * 오늘까지의 할인을 통째로 한 번 곱하면 같은 값이 된다 — 그래서 기록은
125
+ * `{ok, no, at}` 세 칸 그대로다.
126
+ *
127
+ * 아주 작아진 값은 0 으로 떨군다. 안 그러면 10^-9 짜리 부스러기가 표에 남아
128
+ * 자리만 차지하고, 그 부스러기가 `줄이기` 에서 진짜 기록을 밀어낸다.
129
+ */
130
+ export const 부스러기 = 0.01;
131
+
132
+ export function 삭힘({ ok = 0, no = 0 } = {}, 날수 = 0) {
133
+ if (!Number.isFinite(날수)) return { ok: 0, no: 0 };
134
+ const a = (Number(ok) || 0) * Math.pow(하루할인된다, 날수);
135
+ const b = (Number(no) || 0) * Math.pow(하루할인안된다, 날수);
136
+ return { ok: a < 부스러기 ? 0 : a, no: b < 부스러기 ? 0 : b };
137
+ }
138
+
139
+ /*
140
+ * ── 윌슨 점수 구간 ──────────────────────────────────────────────────────
141
+ *
142
+ * 성공 s 번, 전체 n 번일 때 진짜 성공률이 어느 사이에 있는가. 평균을 그대로
143
+ * 쓰지 않는 까닭은 n 이 작을 때 평균이 거짓말을 하기 때문이다 — 1승 0패의
144
+ * 평균은 100% 지만, 그 100% 는 아무것도 안 말해 준다.
145
+ *
146
+ * ── 소수 n 을 넣는 것에 대해 (알고 넘어가는 어긋남) ─────────────────────
147
+ *
148
+ * 윌슨 점수는 **정수 시행**을 전제로 한 근사다. 우리가 넣는 n 은 삭힌 값이라
149
+ * 소수이고, 그건 엄밀히는 맞지 않는다. 가중된 관측의 유효 표본 수는
150
+ * `(Σw)² / Σw²` 인데 그 값은 언제나 `Σw` 보다 작으므로, `Σw` 를 그대로 쓰면
151
+ * **분산을 덜 잡는다** — 즉 우리는 실제보다 조금 더 확신하는 쪽으로 틀린다.
152
+ *
153
+ * 제대로 하려면 관측마다 무게를 들고 있다가 베타 사후분포의 분위수를 봐야
154
+ * 하는데, 그러려면 기록이 `{ok, no, at}` 세 칸으로 안 끝난다. 그 값을 치를
155
+ * 만큼의 정밀도가 아니다 — 여기서 나오는 답은 「말한다 / 안 한다」 둘뿐이다.
156
+ *
157
+ * 대신 **어긋나는 방향을 알고 문턱을 그만큼 넉넉히 잡았다.** 두 번 겪어야
158
+ * 말하기 시작하는데, 덜 잡힌 분산은 그 둘을 하나 반쯤으로 보이게 하는
159
+ * 정도다. 지어낸 정밀도로 한 번 더 계산하느니, 어긋남을 적어 두는 편이 낫다.
160
+ */
161
+ function 구간(s, n) {
162
+ if (!(n > 0)) return { 아래: 0, 위: 1 };
163
+ const p = Math.min(1, Math.max(0, s / n));
164
+ const z2 = z * z;
165
+ const 나눔 = 1 + z2 / n;
166
+ const 가운데 = (p + z2 / (2 * n)) / 나눔;
167
+ const 폭 = (z / 나눔) * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n));
168
+ return { 아래: Math.max(0, 가운데 - 폭), 위: Math.min(1, 가운데 + 폭) };
169
+ }
170
+
171
+ /** 성공률의 아래끝. 표본이 적으면 그만큼 깎인다. */
172
+ export function 윌슨아래(s, n) { return 구간(s, n).아래; }
173
+ /** 성공률의 위끝. 「안 된다」 를 말해도 되는지는 이쪽으로 잰다. */
174
+ export function 윌슨위(s, n) { return 구간(s, n).위; }
175
+
176
+ /**
177
+ * 이 기록을 보고 무엇을 말할 수 있나.
178
+ *
179
+ * @returns {'된다'|'안된다'|'모름'}
180
+ *
181
+ * 「모름」 이 제일 흔한 답이고, 그래야 맞다. 우리가 프롬프트에 한 줄을 더하는
182
+ * 것은 모델의 판단을 그쪽으로 미는 일이라, 확실하지 않으면 안 미는 편이 낫다.
183
+ */
184
+ export function 뭐라할까(기록, 지금) {
185
+ const { ok, no } = 삭힘(기록, 며칠됐나(지금, 기록?.at));
186
+ const n = ok + no;
187
+ if (!(n > 0)) return '모름';
188
+ if (윌슨아래(ok, n) >= 좋음문턱) return '된다';
189
+ if (윌슨위(ok, n) <= 나쁨문턱) return '안된다';
190
+ return '모름';
191
+ }
@@ -1016,6 +1016,58 @@ async function 미리비키기(opts, conn) {
1016
1016
  }
1017
1017
 
1018
1018
  // 한 번에 받기.
1019
+ /*
1020
+ * ── 이 요청이 **어느 자리로 갔나** ──────────────────────────────────────
1021
+ *
1022
+ * 게이트웨이 뒤에는 대개 같은 모델이 여러 자리에 올라가 있다(리전 여럿,
1023
+ * 열쇠 여럿). 그런데 **프리픽스 캐시는 자리마다 따로다.** 그래서 어제까지
1024
+ * 92% 로 읽히던 대화가, 다음 요청이 다른 자리로 가는 순간 0% 가 된다 —
1025
+ * 우리가 보낸 것은 한 글자도 안 달라졌는데.
1026
+ *
1027
+ * 실제로 그 줄을 봤다. 같은 122.4K 프리픽스가 3분 41초 만에 다시 나갔고,
1028
+ * 그 한 번이 그날 제일 비싼 요청이었다. 화면에는 아무 말도 안 났다.
1029
+ *
1030
+ * 그래서 **서버가 말해 줄 때만** 적어 둔다. LiteLLM 은 어느 자리가 받았는지
1031
+ * 를 응답 머리로 알려 준다(`x-litellm-model-id`) — 이 값이 턴 사이에 바뀌면
1032
+ * 캐시가 끊겼다는 뜻이고, 그건 짐작이 아니라 잰 것이다.
1033
+ *
1034
+ * 없으면 아무 말도 안 한다. 모르는 머리는 그냥 없는 것이고, 없는 것을 두고
1035
+ * 「자리가 안 바뀌었다」 고 말하지도 않는다.
1036
+ *
1037
+ * **값은 화면에 안 찍는다.** 이 값은 사내 배포 이름이거나 리전 주소일 수
1038
+ * 있다. 우리가 쓸 것은 「바뀌었나」 하나뿐이라, 바뀌었다는 사실만 올린다.
1039
+ */
1040
+ /*
1041
+ * ── 왜 이름이 하나뿐인가 ────────────────────────────────────────────────
1042
+ *
1043
+ * 세션 이름을 **내보낼** 때는 아는 이름을 다 단다(위 세션머리). 머리는 모르면
1044
+ * 무시되니 이름이 늘어도 잃을 것이 없어서다. 여기는 반대다 — **받아서 읽는**
1045
+ * 자리라, 이름 하나를 잘못 고르면 없는 일을 있다고 말하게 된다.
1046
+ *
1047
+ * 그래서 「어느 자리가 받았나」 를 뜻한다고 **문서에 적힌** 이름만 읽는다.
1048
+ * 그럴듯한 일반형들을 같이 읽어 봤다가 도로 뺐다:
1049
+ *
1050
+ * x-deployment-id 일반 웹·컨테이너 인프라(배포 해시)가 같은 이름을
1051
+ * 쓴다. 모델과 아무 상관 없이 배포할 때마다 바뀌므로,
1052
+ * 멀쩡한 캐시를 두고 「자리가 바뀌었다」 를 찍는다.
1053
+ * x-ms-deployment-name Azure 의 **논리** 배포 이름이라 물리 자리가 바뀌어도
1054
+ * 그대로다. 읽어 봐야 알아내는 것이 없고, 있는데 없다고
1055
+ * 말하는 쪽이라 더 나쁘다.
1056
+ *
1057
+ * 모르면 아무 말도 안 하는 편이 낫다. 안 뜨는 경고는 사람이 다른 데를 보게
1058
+ * 하지 않지만, 틀린 경고는 진짜 경고까지 안 읽게 만든다.
1059
+ */
1060
+ const 자리머리들 = ['x-litellm-model-id'];
1061
+
1062
+ export function 간자리(머리) {
1063
+ if (!머리?.get) return null;
1064
+ for (const 이름 of 자리머리들) {
1065
+ const v = 머리.get(이름);
1066
+ if (v) return String(v).trim() || null;
1067
+ }
1068
+ return null;
1069
+ }
1070
+
1019
1071
  export async function chat(conn, opts) {
1020
1072
  // 이 회사가 받는 모양으로 도구를 다듬는다 (backend/toolfit.js).
1021
1073
  // 모르는 주소면 아무것도 안 바뀐다 — 지금까지와 똑같이 돈다.
@@ -1038,7 +1090,11 @@ export async function chat(conn, opts) {
1038
1090
  할당량기억(r.headers, 할당량자리(conn));
1039
1091
  // 다듬느라 이름을 고쳤으면 여기서 되돌린다. 밖에서는 그런 일이 있었는지
1040
1092
  // 모른 채로 원래 이름을 받는다.
1041
- if (r.ok) return 이름되돌리기(extractMessage(conn.kind, r.json), 맞춘것.되돌림);
1093
+ if (r.ok) {
1094
+ const 것 = 이름되돌리기(extractMessage(conn.kind, r.json), 맞춘것.되돌림);
1095
+ const 자리 = 간자리(r.headers);
1096
+ return 자리 ? { ...것, 간자리: 자리 } : 것;
1097
+ }
1042
1098
  // 열쇠가 늙어서 막힌 것이면 새로 받고 한 번만 다시. 시도 수는 안 올린다 —
1043
1099
  // 서버가 막은 것이 아니라 우리 열쇠가 낡았던 것이라 물러설 까닭이 없다.
1044
1100
  if (열쇠다시받을까(conn, r.status, 열쇠다시받음)) {
@@ -1267,6 +1323,9 @@ export async function* chatStream(conn, opts) {
1267
1323
  */
1268
1324
  if (!acc.content && acc.거절글?.trim()) acc.content = acc.거절글.trim();
1269
1325
  delete acc.거절글;
1326
+ // 흘려 받는 자리에서도 **어느 자리가 받았는지**를 같이 올린다 (위 간자리).
1327
+ const 자리 = 간자리(r.headers ?? r.res?.headers);
1328
+ if (자리) acc.간자리 = 자리;
1270
1329
  yield { type: 'done', message: 이름되돌리기(acc, 맞춘것.되돌림) };
1271
1330
  }
1272
1331
 
package/src/commands.js CHANGED
@@ -2524,7 +2524,22 @@ function 배움명령(session, ctx, arg = '') {
2524
2524
  say(` ${c.bold('이 폴더에서 돌려 본 명령')}`);
2525
2525
  for (const r of 명령.slice(0, 12)) {
2526
2526
  const 표 = r.no === 0 ? c.green('✓') : r.ok === 0 ? c.red('✗') : c.yellow('~');
2527
- say(` ${표} ${pad(r.이름, 24)} ${c.gray(`됨 ${r.ok} · 안 됨 ${r.no}`)}`);
2527
+ /*
2528
+ * 언제 겪었는지와 **지금 실리는지**를 같이 적는다.
2529
+ *
2530
+ * 셈은 삭는다 (agent/신뢰도.js). 그래서 「됨 3 · 안 됨 0」 두 줄이 화면에
2531
+ * 똑같이 보여도 한 줄은 프롬프트에 실리고 다른 줄은 안 실릴 수 있다.
2532
+ * 특히 「안 된다」 는 스스로를 봉인하던 자리라, 그것이 지금 입을 다물고
2533
+ * 있다는 사실이야말로 사람이 봐야 하는 것이다.
2534
+ *
2535
+ * 판정은 배움이 낸 것을 그대로 받는다 — 여기서 다시 계산하면 언젠가
2536
+ * 한쪽만 고쳐지고, 그때부터 화면이 실제와 다른 말을 한다.
2537
+ */
2538
+ const 셈 = (n) => (Number.isFinite(n) ? String(Math.round(n * 10) / 10) : '?');
2539
+ const 날 = Number.isFinite(r.나이) ? `${Math.floor(r.나이)}일 전` : '언제인지 모름';
2540
+ const 말 = r.판정 === '모름' ? `${날} — 아직 안 싣습니다` : `${날} · 프롬프트에 실림`;
2541
+ say(` ${표} ${pad(r.이름, 24)} ${c.gray(`됨 ${셈(r.ok)} · 안 됨 ${셈(r.no)}`)}`
2542
+ + ` ${c.gray(말)}`);
2528
2543
  }
2529
2544
  say('');
2530
2545
  }
package/src/i18n/en.js CHANGED
@@ -474,6 +474,10 @@ export const en = {
474
474
  'run.stopping': 'stopping…',
475
475
  'run.errKept': 'What arrived before the failure was kept in the conversation — you can carry on from there.',
476
476
  'ev.capped': 'The answer was cut off at {한계} tokens — raise the cap with /out, or have files written in parts',
477
+ 'ev.phaseMove': '{왜} — from here on this runs as {새}, not {옛}',
478
+ 'ev.phaseWhyEdit': 'Started changing files',
479
+ 'ev.phaseWhyTodo': 'The plan is written down',
480
+ 'ev.phaseTools': 'More tools from here ({도구}) — the prefix gets written once more',
477
481
  'ev.cutoff': 'The server stopped the answer without saying it had finished — it may have been cut mid-way',
478
482
  'ev.stalled': 'Nothing arrived for {n}s, so the connection was dropped — this is what came through',
479
483
  'ev.nudgeMissed': 'It was about to finish leaving {n} of the things you asked for undone — pushed back once',
package/src/i18n/ja.js CHANGED
@@ -432,6 +432,10 @@ export const ja = {
432
432
  'run.stopping': '止めています…',
433
433
  'run.errKept': 'ここまで届いた分は会話に残しました — このまま続けて話せます。',
434
434
  'ev.capped': '答えが {한계} トークンで切れました — /out で上限を上げるか、ファイルは分けて書かせてください',
435
+ 'ev.phaseMove': '{왜} — ここからは {옛} ではなく {새} で進めます',
436
+ 'ev.phaseWhyEdit': 'ファイルを変更し始めました',
437
+ 'ev.phaseWhyTodo': 'やることを書き終えました',
438
+ 'ev.phaseTools': 'ツールが増えました ({도구}) — 先頭を一度書き直します',
435
439
  'ev.cutoff': 'サーバーが終わったとも言わずに答えを止めました — 途中で切れた可能性があります',
436
440
  'ev.stalled': '{n}秒間なにも届かなかったので切りました — 届いたぶんだけ表示します',
437
441
  'ev.nudgeMissed': '頼んだこと {n} 件を残したまま終えようとしたので、一度押し戻しました',
package/src/i18n/ko.js CHANGED
@@ -505,6 +505,10 @@ export const ko = {
505
505
  * `deel -p` 뒤 스크립트로 넘어가던 것이 그 탓이다.
506
506
  */
507
507
  'ev.capped': '답이 {한계} 토큰에서 잘렸습니다 — /out 으로 상한을 올리거나, 파일은 나눠 쓰게 하세요',
508
+ 'ev.phaseMove': '{왜} — 여기서부터 {옛} 대신 {새} 로 갑니다',
509
+ 'ev.phaseWhyEdit': '파일을 바꾸기 시작했습니다',
510
+ 'ev.phaseWhyTodo': '할 일을 다 적었습니다',
511
+ 'ev.phaseTools': '도구가 늘었습니다 ({도구}) — 앞머리를 한 번 다시 씁니다',
508
512
  'ev.cutoff': '서버가 끝났다는 말 없이 답을 멈췄습니다 — 중간에서 끊겼을 수 있습니다',
509
513
  'ev.stalled': '{n}초 동안 아무것도 안 와서 끊었습니다 — 받은 데까지만 보여 드립니다',
510
514
  'ev.nudgeMissed': '시킨 것 {n}가지를 빼놓고 끝내려고 해서 한 번 되밀었습니다',
package/src/i18n/zh.js CHANGED
@@ -432,6 +432,10 @@ export const zh = {
432
432
  'run.stopping': '正在停止…',
433
433
  'run.errKept': '出错前收到的内容已经留在对话里 — 可以直接接着说。',
434
434
  'ev.capped': '回答在 {한계} 个 token 处被截断 — 用 /out 调高上限,或者让文件分次写',
435
+ 'ev.phaseMove': '{왜} — 从这里开始按 {새} 而不是 {옛} 进行',
436
+ 'ev.phaseWhyEdit': '开始修改文件了',
437
+ 'ev.phaseWhyTodo': '要做的事已经列完了',
438
+ 'ev.phaseTools': '工具变多了 ({도구}) — 前缀要重写一次',
435
439
  'ev.cutoff': '服务端没说结束就停下了 — 回答可能是中途断的',
436
440
  'ev.stalled': '{n} 秒没有任何数据,已断开 — 只显示已收到的部分',
437
441
  'ev.nudgeMissed': '还剩你交代的 {n} 件事没做就要收尾,已经推回去一次',
package/src/oneshot.js CHANGED
@@ -38,7 +38,7 @@ import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
38
38
  import { 잠잠기본 } from './backend/http.js';
39
39
  import { 인증서설정 } from './backend/clientcert.js';
40
40
  import { route } from './agent/route.js';
41
- import { get as getWork } from './agent/modes.js';
41
+ import { get as getWork, 보일이름 } from './agent/modes.js';
42
42
  import { 모두끝내기 as 일감모두끝내기, 일감인자 } from './tools/jobs.js';
43
43
  import { 첫이름 } from './tools/label.js';
44
44
 
@@ -612,6 +612,21 @@ export async function runOnce(opts = {}) {
612
612
  곁(` ${c.cyan('⚙')} ${c.gray(옮긴말('loop.wire', { 무엇: ev.무엇 }))}`);
613
613
  break;
614
614
 
615
+ /*
616
+ * 종합 모드가 턴 도중에 단계를 옮겼다 (agent/단계.js).
617
+ *
618
+ * 한 방 실행에서 특히 남겨야 하는 줄이다. `deel -p` 는 잡·CI 에서 돌고
619
+ * 그 기록만 나중에 남는다 — 「계획만 내고 끝날 줄 알았는데 파일이
620
+ * 바뀌어 있다」 를 되짚을 자리가 여기 말고는 없다.
621
+ */
622
+ case '단계옮김':
623
+ 곁(` ${c.hcyan('→')} ${c.gray(옮긴말('ev.phaseMove', {
624
+ 왜: 옮긴말(ev.왜 === '바꿈' ? 'ev.phaseWhyEdit' : 'ev.phaseWhyTodo'),
625
+ 옛: 보일이름(ev.옛),
626
+ 새: 보일이름(ev.새),
627
+ }))}`);
628
+ break;
629
+
615
630
  case 'folded':
616
631
  곁(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 (${ev.아낀토큰.toLocaleString()} 토큰을 비움)`)}`);
617
632
  break;
package/src/repl.js CHANGED
@@ -2120,6 +2120,48 @@ export async function chatLoop(opts = {}) {
2120
2120
  break;
2121
2121
  }
2122
2122
 
2123
+ /*
2124
+ * 게이트웨이가 이번 요청을 **다른 자리로** 보냈다.
2125
+ *
2126
+ * 프리픽스 캐시는 자리마다 따로라, 이 줄이 뜬 턴은 우리가 보낸 것이
2127
+ * 한 글자도 안 달라졌어도 앞머리를 통째로 다시 쓴다. 청구서에서
2128
+ * 「왜 이 한 줄만 열 배냐」 로 보이던 것이 이것이다.
2129
+ *
2130
+ * 값은 안 찍는다 — 사내 배포 이름이나 리전 주소일 수 있다. 우리가
2131
+ * 쓸 것은 「바뀌었다」 하나뿐이다 (backend/adapter.js 의 간자리).
2132
+ */
2133
+ case '자리바뀜':
2134
+ say(` ${c.hyellow('⇄')} ${c.gray('게이트웨이가 이번 요청을 다른 자리로 보냈습니다 — 앞머리를 캐시에서 못 읽고 다시 씁니다.')}`);
2135
+ if (session.level !== '쉬움') {
2136
+ say(` ${c.gray('한 대화를 한 자리에 붙여 두는 설정이 게이트웨이 쪽에 있습니다 (세션 고정).')}`);
2137
+ }
2138
+ break;
2139
+
2140
+ /*
2141
+ * 종합 모드가 **단계를 옮겼다** (agent/단계.js).
2142
+ *
2143
+ * 이 줄이 없으면 사람은 화면의 모드 표시가 턴 도중에 왜 바뀌었는지
2144
+ * 모른다 — 자기가 누른 적도 없는데 `~디버그` 가 `~코드` 로 바뀌어
2145
+ * 있으면 그건 알림이 아니라 버그처럼 보인다. 그래서 무엇을 보고
2146
+ * 옮겼는지(`왜`) 를 같이 적는다.
2147
+ *
2148
+ * 도구가 늘어난 경우는 한 줄 더 붙인다. 규격이 도구 목록을 앞머리
2149
+ * 맨 앞에 렌더하므로, 도구가 하나 늘면 그 턴은 앞머리를 캐시에서 못
2150
+ * 읽고 다시 쓴다. 접기(folded)에서 그 대가를 적기 시작한 것과 같은
2151
+ * 이유다 — 이득만 보이고 값은 며칠 뒤 청구서에서 보는 일이 없게.
2152
+ */
2153
+ case '단계옮김': {
2154
+ const 왜 = 옮긴말(ev.왜 === '바꿈' ? 'ev.phaseWhyEdit' : 'ev.phaseWhyTodo');
2155
+ say(` ${c.hcyan('→')} ${c.gray(옮긴말('ev.phaseMove', { 왜, 옛: 보일이름(ev.옛), 새: 보일이름(ev.새) }))}`);
2156
+ if ((ev.늘어난도구?.length ?? 0) > 0 && session.level !== '쉬움') {
2157
+ const 보일수 = Math.min(5, ev.늘어난도구.length);
2158
+ const 이름들 = ev.늘어난도구.slice(0, 보일수).join(' · ')
2159
+ + (ev.늘어난도구.length > 보일수 ? ` +${ev.늘어난도구.length - 보일수}` : '');
2160
+ say(` ${c.gray(옮긴말('ev.phaseTools', { 도구: 이름들 }))}`);
2161
+ }
2162
+ break;
2163
+ }
2164
+
2123
2165
  case 'trimmed':
2124
2166
  say(` ${c.gray(`(컨텍스트가 차서 오래된 대화 ${ev.dropped}개를 줄였습니다)`)}`);
2125
2167
  break;
@@ -2148,6 +2190,16 @@ export async function chatLoop(opts = {}) {
2148
2190
  clearThinking();
2149
2191
  say(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 — `)}`
2150
2192
  + `${c.white(ev.아낀토큰.toLocaleString())} ${c.gray('토큰을 비웠습니다. 대화는 그대로입니다.')}`);
2193
+ /*
2194
+ * 접기는 **값이 드는 일**이다 (agent/compact.js 의 손익분기).
2195
+ *
2196
+ * 이력 가운데 글을 바꿔치므로 그 자리 뒤가 전부 새 글이 되고,
2197
+ * 캐시가 도는 연결에서는 싸게 읽히던 것을 비싸게 다시 쓴다.
2198
+ * 여태 화면에는 이득만 나왔다 — 대가는 며칠 뒤 청구서에서 봤다.
2199
+ */
2200
+ if (ev.캐시다시씀) {
2201
+ say(` ${c.gray('이번 턴은 앞머리를 캐시에서 못 읽고 다시 씁니다 — 값과 지연이 한 번 튑니다.')}`);
2202
+ }
2151
2203
  /*
2152
2204
  * 무엇을 접었는지 이름으로 보여 준다.
2153
2205
  *
@@ -2244,7 +2296,7 @@ export async function chatLoop(opts = {}) {
2244
2296
  clearThinking();
2245
2297
  if (streamed) { 답비우기(); say(''); streamed = false; }
2246
2298
  say(` ${mark.warn} ${c.gray(`답이 ${c.white(ev.cap.toLocaleString())} 토큰에서 잘렸습니다`)}`
2247
- + `${ev.정한값 ? c.gray(' (직접 정하신 상한입니다)') : c.gray(' — 올리는 천장입니다')}`);
2299
+ + `${ev.정한값 ? c.gray(' (직접 정하신 상한입니다)') : c.gray(' — 서버가 알려 값이 없어 선 기본 상한입니다')}`);
2248
2300
  say(` ${c.gray('한 번에 더 길게 받으려면')} ${c.cyan('/out 32k')}${c.gray('. 파일을 쓰는 중이었다면 Append 로 나눠 쓰게 하세요.')}`);
2249
2301
  break;
2250
2302
 
@@ -2478,7 +2530,29 @@ export async function chatLoop(opts = {}) {
2478
2530
  if (tools) bits.push(`${옮긴말('scr.tools')} ${옮긴말('unit.calls', { n: tools })}`);
2479
2531
  const dIn = (session.usage.prompt || session.usage.in) - before.in;
2480
2532
  const dOut = session.usage.out - before.out;
2533
+ const d읽음 = (session.usage.cacheRead ?? 0) - before.cacheRead;
2534
+ const d씀 = (session.usage.cacheWrite ?? 0) - before.cacheWrite;
2481
2535
  if (dIn || dOut) bits.push(`↑${dIn.toLocaleString()} ↓${dOut.toLocaleString()}`);
2536
+ /*
2537
+ * ── ↑ 안을 가른다 ────────────────────────────────────────────────────
2538
+ *
2539
+ * `↑798,620` 한 줄만 보면 그게 다 정가로 나간 것처럼 읽힌다. 실제로는
2540
+ * 그중 대부분이 **캐시에서 읽힌 것**이고, 우리가 재 본 자리에서 읽기는
2541
+ * 쓰기의 1/12.5 였다. 즉 이 두 숫자는 자릿수가 다른 돈이다.
2542
+ *
2543
+ * 이걸 안 가르면 사람이 할 수 있는 판단이 없다. 가르면 바로 보인다 —
2544
+ * 「새로 쓴 것」 이 크면 캐시가 끊긴 것이고(자리가 바뀌었거나 접었거나),
2545
+ * 「캐시에서」 가 크면 대화가 길 뿐 값은 안 나가고 있는 것이다.
2546
+ *
2547
+ * 잰 값이 없으면 안 적는다. 캐시를 안 세어 주는 서버에서 0 을 적어 두면
2548
+ * 「캐시가 하나도 안 걸렸다」 로 읽히는데, 그건 우리가 모르는 것이다.
2549
+ */
2550
+ if (d읽음 > 0 || d씀 > 0) {
2551
+ const 몫 = [];
2552
+ if (d읽음 > 0) 몫.push(`캐시에서 ${d읽음.toLocaleString()}`);
2553
+ if (d씀 > 0) 몫.push(`새로 씀 ${d씀.toLocaleString()}`);
2554
+ bits.push(`↑ 안: ${몫.join(' · ')}`);
2555
+ }
2482
2556
  /*
2483
2557
  * 이번 턴에 나간 돈.
2484
2558
  *