deel-local-cli 1.20.12 → 2.0.0
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/README.ko.md +96 -77
- package/README.md +119 -80
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +62 -13
- package/src/acp/serve.js +338 -30
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +506 -32
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +300 -0
- package/src/commands/model.js +852 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +452 -39
- package/src/configexplain.js +125 -14
- package/src/doctor.js +164 -26
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +507 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +11 -1
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +444 -36
- package/src/tools/fig.js +40 -9
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1029 -145
- package/src/tools/jobs.js +325 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +366 -56
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +312 -43
- package/src/tools/webfetch.js +184 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/agent/store.js
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
import {
|
|
16
16
|
existsSync, mkdirSync, readdirSync, readFileSync,
|
|
17
17
|
appendFileSync, writeFileSync, statSync, rmSync, chmodSync, renameSync,
|
|
18
|
+
openSync, readSync, closeSync,
|
|
18
19
|
} from 'node:fs';
|
|
19
20
|
import { join } from 'node:path';
|
|
21
|
+
import { 도구결과인가 } from '../backend/adapter.js';
|
|
22
|
+
import { BOM떼기 } from '../safety/trust.js';
|
|
20
23
|
|
|
21
24
|
export const sessionsDir = (root) => join(root, '.deel', 'sessions');
|
|
22
25
|
|
|
@@ -34,29 +37,60 @@ export function newId(at = new Date()) {
|
|
|
34
37
|
* 그러면 서로 다른 두 대화가 한 파일에 섞여 들어가고 되살릴 때 뒤엉킨다.
|
|
35
38
|
* 이미 있는 이름이면 뒤에 번호를 붙여 피한다.
|
|
36
39
|
*/
|
|
37
|
-
export function freeId(dir, at = new Date()) {
|
|
40
|
+
export function freeId(dir, at = new Date(), 피할것 = null) {
|
|
41
|
+
/*
|
|
42
|
+
* ── 파일이 없다고 빈 이름은 아니다 (사냥5 H5-1) ─────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* 여기는 **이 폴더의** 대화 파일만 봤다. 에디터 프로세스 하나가 두 프로젝트를
|
|
45
|
+
* 같은 초에 열면 두 폴더 다 그 이름의 파일이 없어서 둘 다 같은 이름을 받았고,
|
|
46
|
+
* ACP 는 그 이름 하나로 방을 찾으므로 A 탭의 말이 B 방으로 갔다. 부르는 쪽이
|
|
47
|
+
* 「이 프로세스에서 이미 쓴 이름」 을 알려 주면 그것도 피한다.
|
|
48
|
+
*/
|
|
49
|
+
const 비었나 = (id) => !existsSync(join(dir, `${id}.jsonl`)) && !(typeof 피할것 === 'function' && 피할것(id));
|
|
38
50
|
const base = newId(at);
|
|
39
|
-
if (
|
|
51
|
+
if (비었나(base)) return base;
|
|
40
52
|
for (let n = 2; n < 1000; n++) {
|
|
41
53
|
const id = `${base}-${n}`;
|
|
42
|
-
if (
|
|
54
|
+
if (비었나(id)) return id;
|
|
43
55
|
}
|
|
44
56
|
// 여기까지 왔으면 마지막 이름도 비어 있는지 봐야 한다. 안 보고 돌려주면
|
|
45
57
|
// begin() 이 같은 이름으로 쉰 번을 되풀이하다 **아무 파일도 없이** 대화를
|
|
46
58
|
// 시작한다 — 저장한다고 적어 놓고 한 줄도 안 남는 판이 그때 만들어진다.
|
|
47
59
|
for (let n = 0; n < 1000; n++) {
|
|
48
60
|
const id = n ? `${base}-${process.pid}-${n}` : `${base}-${process.pid}`;
|
|
49
|
-
if (
|
|
61
|
+
if (비었나(id)) return id;
|
|
50
62
|
}
|
|
51
63
|
return `${base}-${process.pid}-${Date.now().toString(36)}`;
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
/**
|
|
67
|
+
* 대화 이름으로 쓸 수 있나 — 이름이 곧 `.deel/sessions/<이름>.jsonl` 의 한 조각이다.
|
|
68
|
+
*
|
|
69
|
+
* 이름을 경로에 그대로 이어 붙인다. 여태 이름을 본 자리는 remove() 하나였고,
|
|
70
|
+
* 읽기·적기·갈아 끼우기는 안 봤다. 그래서 에디터가 session/load 로 `../../evil/x`
|
|
71
|
+
* 를 주면 대화 폴더 **밖**의 `evil/x.jsonl` 을 대화로 읽었고, 한 턴이 돌면 거기에
|
|
72
|
+
* 대화를 적었다. 이름을 받는 문이 셋(--resume · session/load · /sessions)이라,
|
|
73
|
+
* 검사는 이름이 경로가 되는 한 자리(Store)와 여기 하나에 둔다.
|
|
74
|
+
*/
|
|
75
|
+
export function 대화이름인가(id) {
|
|
76
|
+
const s = String(id ?? '');
|
|
77
|
+
return /^[^\\/:*?"<>|\x00-\x1f]+$/.test(s) && s.trim() === s && !s.includes('..') && !/^\.+$/.test(s);
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
export class Store {
|
|
55
|
-
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} root
|
|
83
|
+
* @param {string|null} id 이어 쓸 대화 이름. 안 주면 빈 이름을 지어 새로 연다
|
|
84
|
+
* @param {object} [o]
|
|
85
|
+
* @param {(id: string) => boolean} [o.피할것] 이름을 지을 때 파일이 없어도 피할 이름
|
|
86
|
+
* (사냥5 H5-1 — 한 프로세스가 여러 폴더의 대화를 열 때. freeId 머리말)
|
|
87
|
+
*/
|
|
88
|
+
constructor(root, id = null, { 피할것 = null } = {}) {
|
|
56
89
|
this.root = root;
|
|
57
90
|
this.dir = sessionsDir(root);
|
|
58
91
|
this.auto = id == null; // 우리가 지은 이름인가 (그러면 겹칠 때 바꿔도 된다)
|
|
59
|
-
this
|
|
92
|
+
this.피할것 = typeof 피할것 === 'function' ? 피할것 : null;
|
|
93
|
+
this.#use(id ?? freeId(this.dir, new Date(), this.피할것));
|
|
60
94
|
this.opened = false;
|
|
61
95
|
/** 적으려다 못 적은 줄 수와 첫 까닭. 0 이 아니면 이 대화는 반만 남는다. */
|
|
62
96
|
this.못쓴수 = 0;
|
|
@@ -71,8 +105,48 @@ export class Store {
|
|
|
71
105
|
|
|
72
106
|
#use(id) {
|
|
73
107
|
this.id = id;
|
|
74
|
-
|
|
108
|
+
/*
|
|
109
|
+
* 쓸 수 없는 이름이면 **파일 자리를 안 만든다.** 경로를 지어 두면 그 경로를
|
|
110
|
+
* 쓰는 자리가 하나라도 검사를 빠뜨리는 순간 폴더 밖이 열린다. 없는 자리는
|
|
111
|
+
* 아무도 못 쓴다 — 읽기·적기·갈아 끼우기가 전부 이름틀림 을 보고 물러선다.
|
|
112
|
+
*/
|
|
113
|
+
this.이름틀림 = !대화이름인가(id);
|
|
114
|
+
this.file = this.이름틀림 ? null : join(this.dir, `${id}.jsonl`);
|
|
75
115
|
this.#잠갔나 = false; // 다른 파일로 옮겨 갔으면 그 파일은 아직 안 잠갔다
|
|
116
|
+
this.#끝자리 = null; // 옮겨 간 파일이 어디까지 적혀 있는지는 아직 모른다
|
|
117
|
+
this.#줄끝봤나 = false; // 옮겨 간 파일이 줄 끝으로 끝나는지도 아직 안 봤다
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* ── 같은 대화를 창 두 개가 적고 있나 ────────────────────────────────────
|
|
122
|
+
*
|
|
123
|
+
* `--continue` 로 창을 둘 여는 것은 이상한 짓이 아니다. 한쪽에서 고치는 동안
|
|
124
|
+
* 한쪽에서 물어보려고 흔히 그렇게 한다. 그런데 그때 두 대화가 **한 파일에**
|
|
125
|
+
* 줄줄이 섞여 들어갔다. 섞인 것만도 이어받기가 뒤엉키는데, 한쪽이 접기까지
|
|
126
|
+
* 하면 replace() 가 파일을 통째로 다시 쓰므로 다른 창의 대화는 그 한 번에
|
|
127
|
+
* 없어졌다. 없어진 쪽의 `적은할일`·`적은요청` 은 이미 적었다고 들고 있어서,
|
|
128
|
+
* 남은 할 일과 시킨 말도 두 번 다시 안 적힌다.
|
|
129
|
+
*
|
|
130
|
+
* 그동안 화면은 아무 말이 없었다. 쓰기가 실패한 적이 없으니 못쓴수 가 0 이고,
|
|
131
|
+
* 그래서 「안 적히고 있습니다」 가 한 번도 안 떴다. 사람이 알아차리는 자리는
|
|
132
|
+
* 또 **다음 날 --resume** 이다 — 이 파일 위쪽이 너무 늦다고 적어 둔 그 자리다.
|
|
133
|
+
*
|
|
134
|
+
* 우리가 지은 이름은 freeId 와 'wx' 가 이미 막는다. 사람이 이름을 준 자리
|
|
135
|
+
* (--resume/--continue)만 여기서 본다. 내가 적어 둔 데까지의 바이트 수를
|
|
136
|
+
* 들고 있다가, 적기 전에 파일이 그보다 자라 있으면 남이 쓴 것이다.
|
|
137
|
+
*
|
|
138
|
+
* 그러면 **안 적고 센다.** 그냥 이어 붙이는 것은 두 대화를 섞어 놓는 일이고,
|
|
139
|
+
* 섞인 파일은 둘 다 못 쓰게 된다. 대신 잠자코 물러나지는 않는다 — 못쓴 셈에
|
|
140
|
+
* 올려 두면 화면이 바로 다음 줄에 「이 대화가 파일에 안 적히고 있습니다」 를
|
|
141
|
+
* 말한다. 창을 닫기 **전에** 알아야 사람이 손을 쓴다.
|
|
142
|
+
*/
|
|
143
|
+
#끝자리 = null;
|
|
144
|
+
/** 지금 파일 크기. 못 재면 null — 모르는 것은 모른다고 둔다(헛경보가 더 나쁘다). */
|
|
145
|
+
#크기() { try { return statSync(this.file).size; } catch { return null; } }
|
|
146
|
+
#남이썼나() {
|
|
147
|
+
if (this.auto || this.#끝자리 == null) return false;
|
|
148
|
+
const 지금 = this.#크기();
|
|
149
|
+
return 지금 != null && 지금 !== this.#끝자리;
|
|
76
150
|
}
|
|
77
151
|
|
|
78
152
|
/*
|
|
@@ -161,19 +235,27 @@ export class Store {
|
|
|
161
235
|
* 사용자가 --resume 으로 이름을 준 경우에는 옮기지 않고 그 파일에 이어 쓴다.
|
|
162
236
|
*/
|
|
163
237
|
begin(meta) {
|
|
238
|
+
// 쓸 수 없는 이름이면 머리글도 안 적는다(#use). 저장된다고 말하지 않게 센다.
|
|
239
|
+
if (this.이름틀림) { this.#못썼다({ code: 'BAD_NAME' }); return this; }
|
|
164
240
|
this.#open();
|
|
165
241
|
const head = JSON.stringify({ t: 'meta', at: new Date().toISOString(), ...meta }) + '\n';
|
|
166
242
|
for (let tries = 0; tries < 50; tries++) {
|
|
167
243
|
try {
|
|
168
244
|
writeFileSync(this.file, head, { encoding: 'utf8', flag: 'wx' });
|
|
169
245
|
this.#잠그기();
|
|
246
|
+
this.#끝자리 = this.#크기();
|
|
170
247
|
return this;
|
|
171
248
|
} catch (err) {
|
|
172
249
|
// 못 적어도 대화는 계속돼야 한다. 다만 조용히 넘기지는 않는다 —
|
|
173
250
|
// 머리글을 못 적었으면 그 뒤의 줄도 십중팔구 못 적는다.
|
|
174
251
|
if (err?.code !== 'EEXIST') { this.#못썼다(err); return this; }
|
|
175
|
-
if (!this.auto)
|
|
176
|
-
|
|
252
|
+
if (!this.auto) {
|
|
253
|
+
// 이어쓰기 — 이미 있는 게 맞다. 다만 **지금 어디까지 적혀 있는지**를
|
|
254
|
+
// 기억해 둔다. 다음에 그보다 자라 있으면 다른 창이 같은 대화를 연 것이다.
|
|
255
|
+
this.#끝자리 = this.#크기();
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
this.#use(freeId(this.dir, new Date(), this.피할것)); // 누가 채 갔다. 옆자리로.
|
|
177
259
|
}
|
|
178
260
|
}
|
|
179
261
|
// 쉰 번을 다 써도 자리를 못 잡았다. 여기서 잠자코 돌아가면 화면은
|
|
@@ -182,13 +264,57 @@ export class Store {
|
|
|
182
264
|
return this;
|
|
183
265
|
}
|
|
184
266
|
|
|
267
|
+
/*
|
|
268
|
+
* ── 반쪽 줄 뒤에 이어 적지 않는다 ─────────────────────────────────────────
|
|
269
|
+
*
|
|
270
|
+
* 도중에 죽으면 마지막 줄이 개행 없이 반만 남는다. 읽을 때는 건너뛰니 앞부분은
|
|
271
|
+
* 성하다(이 파일 머리말). 그런데 --resume 으로 그 파일에 이어 적으면 첫 새 줄이
|
|
272
|
+
* 그 반쪽 **뒤에 그대로 붙어** 한 줄이 되고, 그 줄은 JSON 이 아니라 다음에 읽을 때
|
|
273
|
+
* 통째로 건너뛴다 — 이어받은 뒤 사람이 처음 한 말이 조용히 없어졌다.
|
|
274
|
+
* 파일마다 처음 적을 때 한 번, 끝이 개행인지 보고 아니면 개행부터 붙인다.
|
|
275
|
+
*/
|
|
276
|
+
#줄끝봤나 = false;
|
|
277
|
+
#줄로끝나나() {
|
|
278
|
+
let fd = null;
|
|
279
|
+
try {
|
|
280
|
+
const 크기 = statSync(this.file).size;
|
|
281
|
+
if (!크기) return true;
|
|
282
|
+
fd = openSync(this.file, 'r');
|
|
283
|
+
const 한바이트 = Buffer.alloc(1);
|
|
284
|
+
readSync(fd, 한바이트, 0, 1, 크기 - 1);
|
|
285
|
+
return 한바이트[0] === 0x0a;
|
|
286
|
+
} catch { return true; } // 없거나 못 읽으면 붙일 반쪽도 없다
|
|
287
|
+
finally { if (fd != null) { try { closeSync(fd); } catch { /* 닫다 터져도 적기는 한다 */ } } }
|
|
288
|
+
}
|
|
289
|
+
|
|
185
290
|
/** 한 줄 적는다. 적었으면 true. 못 적었으면 false 이고 셈에 오른다. */
|
|
186
291
|
#write(obj) {
|
|
292
|
+
// 쓸 수 없는 이름이면 파일 자리가 없다(#use). 적은 척하지 않고 센다.
|
|
293
|
+
if (this.이름틀림) { this.#못썼다({ code: 'BAD_NAME' }); return false; }
|
|
294
|
+
let 줄 = JSON.stringify(obj) + '\n';
|
|
295
|
+
// 남이 같은 파일을 적고 있으면 여기서 선다 (위 「같은 대화를 창 두 개가」).
|
|
296
|
+
if (this.#남이썼나()) { this.#못썼다({ code: 'OTHER_WINDOW' }); return false; }
|
|
187
297
|
try {
|
|
188
|
-
|
|
298
|
+
if (!this.#줄끝봤나) {
|
|
299
|
+
if (!this.#줄로끝나나()) 줄 = '\n' + 줄;
|
|
300
|
+
this.#줄끝봤나 = true;
|
|
301
|
+
}
|
|
302
|
+
appendFileSync(this.file, 줄, 'utf8');
|
|
189
303
|
this.#잠그기();
|
|
304
|
+
// 적은 만큼만 더한다. 다시 재지 않는 것은 이게 줄마다 도는 자리여서다.
|
|
305
|
+
this.#끝자리 = this.#끝자리 == null ? this.#크기() : this.#끝자리 + Buffer.byteLength(줄, 'utf8');
|
|
190
306
|
return true;
|
|
191
|
-
} catch (err) {
|
|
307
|
+
} catch (err) {
|
|
308
|
+
/*
|
|
309
|
+
* 막혔으면 끝을 다시 보게 되돌린다 (2.0.0 6회차 · Gemini 저장6w-a2 X1). 본 표시를
|
|
310
|
+
* 적기 **전에** 세워서, 첫 적기가 막히면(읽기 전용 · 잠김 · 디스크 가득) 다음 줄이
|
|
311
|
+
* 반쪽 줄 뒤에 그대로 붙었다 — 그 줄은 적었다고 true 를 돌려주고 셈에도 안 오르는데,
|
|
312
|
+
* 다시 읽으면 JSON 이 아니라 사라진다. 반쯤 적히고 막힌 경우도 그 반쪽을 다음에 본다.
|
|
313
|
+
*/
|
|
314
|
+
this.#줄끝봤나 = false; // 막혔으니 다음 적기에서 끝을 다시 본다
|
|
315
|
+
this.#못썼다(err);
|
|
316
|
+
return false; // 계속은 하되, 몇 줄을 잃었는지는 센다
|
|
317
|
+
}
|
|
192
318
|
}
|
|
193
319
|
|
|
194
320
|
// 메시지 하나를 덧붙인다. 대화가 진행되는 대로 즉시 남긴다.
|
|
@@ -233,9 +359,23 @@ export class Store {
|
|
|
233
359
|
// 적힌 것이 없으면 세션이 들고 있던 것을 그대로 둔다. 빈 값으로 덮으면
|
|
234
360
|
// 새 갈래를 열자마자 방금 시킨 말이 사라진다 — 갈래마다 파일이 따로다.
|
|
235
361
|
if (할일) session.할일 = 할일;
|
|
236
|
-
if (이번요청)
|
|
237
|
-
|
|
238
|
-
|
|
362
|
+
// `if (이번요청)` 이었다 — 파일에 적힌 **빈 글**(`''`)을 못 되살렸다 (8회차 판정).
|
|
363
|
+
// 아래 507–508 이 적어 둔 대로 `null` 은 「적힌 적이 없다」 이고 `''` 는 「빈 것으로
|
|
364
|
+
// 적혔다」 다. 그 둘을 가르기로 해 놓고 여기서만 안 갈랐다.
|
|
365
|
+
if (이번요청 != null) session.이번요청 = 이번요청;
|
|
366
|
+
/*
|
|
367
|
+
* 표식은 **파일에 적힌 것**으로 세운다 (8회차 판정).
|
|
368
|
+
*
|
|
369
|
+
* 여기가 `JSON.stringify(session.할일 ?? [])` 였다. 세션 값으로 세우면, 파일에
|
|
370
|
+
* 한 줄도 안 적고 「여기까지 적었다」 고 표시한 것이 된다. 그러면 뒤이은
|
|
371
|
+
* `살림적기` 가 「안 바뀜」 으로 보고 **영영 안 적는다** — 그 갈래의 남은 할 일과
|
|
372
|
+
* 시킨 말이 통째로 사라진다. 바로 아래 살림적기 머리말이 적어 둔 그 길이다.
|
|
373
|
+
*
|
|
374
|
+
* 파일이 빈 것과 세션이 빈 것이 같을 때는 아무것도 안 적는다 — 「안 바뀌었으면
|
|
375
|
+
* 한 줄도 안 늘린다」 는 약속은 그대로 지킨다.
|
|
376
|
+
*/
|
|
377
|
+
this.적은할일 = JSON.stringify(할일 ?? []);
|
|
378
|
+
this.적은요청 = String(이번요청 ?? '');
|
|
239
379
|
}
|
|
240
380
|
|
|
241
381
|
/**
|
|
@@ -295,6 +435,13 @@ export class Store {
|
|
|
295
435
|
|
|
296
436
|
// 압축이 일어나면 이력이 통째로 바뀐다. 그때는 새로 적는다.
|
|
297
437
|
replace(messages, note = '압축') {
|
|
438
|
+
/*
|
|
439
|
+
* 다른 창이 같은 파일을 적고 있으면 **여기가 제일 크게 잃는 자리**다.
|
|
440
|
+
* 아래는 파일을 통째로 갈아 끼우므로, 그 한 번에 남의 대화가 통째로
|
|
441
|
+
* 없어진다. 이어 붙이기와 달리 되돌릴 조각조차 안 남는다. 안 쓰고 센다.
|
|
442
|
+
*/
|
|
443
|
+
if (this.이름틀림) { this.#못썼다({ code: 'BAD_NAME' }); return; } // 파일 자리가 없다(#use)
|
|
444
|
+
if (this.#남이썼나()) { this.#못썼다({ code: 'OTHER_WINDOW' }); return; }
|
|
298
445
|
this.#open();
|
|
299
446
|
const meta = this.readMeta() ?? {};
|
|
300
447
|
// 여기가 놓치기 쉬운 자리다. 파일을 새로 쓰면서 못 박은 것을 안 옮기면,
|
|
@@ -313,7 +460,8 @@ export class Store {
|
|
|
313
460
|
lines.push(JSON.stringify({ t: 'note', at: new Date().toISOString(), note }));
|
|
314
461
|
if (못박은것.length) lines.push(JSON.stringify({ t: 'pins', at: new Date().toISOString(), 목록: 못박은것 }));
|
|
315
462
|
if (할일) lines.push(JSON.stringify({ t: 'todo', at: new Date().toISOString(), 목록: 할일 }));
|
|
316
|
-
if (이번요청)
|
|
463
|
+
// `if (이번요청)` 이었다 — 빈 글(`''`)이 옮겨지지 않아 replace() 를 지나면 null 로 변질됐다 (8회차 판정).
|
|
464
|
+
if (이번요청 != null) lines.push(JSON.stringify({ t: 'request', at: new Date().toISOString(), 글: 이번요청 }));
|
|
317
465
|
for (const m of messages) lines.push(JSON.stringify({ t: 'msg', m }));
|
|
318
466
|
/*
|
|
319
467
|
* 옆에 다 쓰고 나서 **한 번에 갈아 끼운다.**
|
|
@@ -334,6 +482,7 @@ export class Store {
|
|
|
334
482
|
renameSync(옆, this.file);
|
|
335
483
|
this.#잠갔나 = false;
|
|
336
484
|
this.#잠그기();
|
|
485
|
+
this.#끝자리 = this.#크기(); // 통째로 바뀌었으니 어디까지인지도 다시 잰다
|
|
337
486
|
} catch (err) {
|
|
338
487
|
try { rmSync(옆, { force: true }); } catch { /* 못 치워도 원본은 성하다 */ }
|
|
339
488
|
this.#못썼다(err);
|
|
@@ -343,7 +492,8 @@ export class Store {
|
|
|
343
492
|
readMeta() {
|
|
344
493
|
if (!existsSync(this.file)) return null;
|
|
345
494
|
try {
|
|
346
|
-
|
|
495
|
+
// 머리글 줄도 BOM 을 떼고 본다 — 까닭은 load() 에.
|
|
496
|
+
const first = BOM떼기(readFileSync(this.file, 'utf8')).split('\n', 1)[0];
|
|
347
497
|
const j = JSON.parse(first);
|
|
348
498
|
return j.t === 'meta' ? j : null;
|
|
349
499
|
} catch { return null; }
|
|
@@ -360,7 +510,12 @@ export class Store {
|
|
|
360
510
|
load() {
|
|
361
511
|
if (!existsSync(this.file)) return { meta: null, messages: [] };
|
|
362
512
|
let 글;
|
|
363
|
-
|
|
513
|
+
/*
|
|
514
|
+
* BOM 을 떼고 읽는다. 윈도우 메모장이나 파워셸로 대화 파일을 열었다 저장하면 첫 줄 앞에
|
|
515
|
+
* BOM 이 붙고, 그 줄(머리글)이 JSON 으로 안 풀려 목록의 모델이 `?` 가 됐다. 접을 때
|
|
516
|
+
* replace() 는 readMeta() 로 머리글을 옮기므로 모델·폴더가 **없는** 머리글을 새로 썼다.
|
|
517
|
+
*/
|
|
518
|
+
try { 글 = BOM떼기(readFileSync(this.file, 'utf8')); }
|
|
364
519
|
catch (err) { return { meta: null, messages: [], 못읽음: err?.code ?? err?.message ?? String(err) }; }
|
|
365
520
|
let meta = null;
|
|
366
521
|
const messages = [];
|
|
@@ -392,7 +547,30 @@ function firstAsk(messages) {
|
|
|
392
547
|
* 그래서 건너뛰되, 남는 것이 하나도 없으면 첫 마디를 그대로 쓴다.
|
|
393
548
|
* 알아보기 어려운 제목이 「빈 대화」 라는 거짓말보다 낫다.
|
|
394
549
|
*/
|
|
395
|
-
|
|
550
|
+
/*
|
|
551
|
+
* 건너뛰는 자를 **한 줄 통째**로 좁힌다 (2.0.0 8회차 판정).
|
|
552
|
+
*
|
|
553
|
+
* `startsWith('[')` 하나로는 두 가지가 같이 틀렸다.
|
|
554
|
+
*
|
|
555
|
+
* 1. 사람이 「[급함] 로그인이 안 됩니다」 로 열고 한 마디를 더 하면, 첫 마디를
|
|
556
|
+
* 건너뛰고 **둘째 마디**가 제목이 된다 — 처음 물은 것이 목록에서 사라진다.
|
|
557
|
+
* 위 문단이 「남는 것이 없으면」 만 보았지, 남는 것이 있을 때는 그대로였다.
|
|
558
|
+
* 2. 남는 것이 없을 때 `?? 쓸만한[0]` 이 **우리가 끼운 글**을 제목으로 골랐다.
|
|
559
|
+
* 사람이 한 적 없는 말이 그 대화의 이름이 됐다.
|
|
560
|
+
*
|
|
561
|
+
* 가르는 자리는 분명하다 — 우리가 끼우는 글은 **첫 줄이 통째로 대괄호**다.
|
|
562
|
+
* 사람이 적는 「[급함] …」 은 닫는 괄호 뒤에 할 말이 이어진다.
|
|
563
|
+
*/
|
|
564
|
+
const 통째괄호 = (글) => /^\[[^\]\n]*\]\s*$/.test(String(글).trim().split(String.fromCharCode(10))[0]);
|
|
565
|
+
/*
|
|
566
|
+
* 우리가 끼우는 글은 이 둘뿐이다 (compact.js 의 접은 요약 · commands.js 의 딴 모델 답).
|
|
567
|
+
* 늘리면 여기도 같이 늘린다 — 안 늘리면 그 글이 대화 이름으로 뜬다.
|
|
568
|
+
*/
|
|
569
|
+
const 우리글 = [/^\[앞선 대화 .*요약해 접었습니다/, /^\[.* 에게 따로 물어본 결과다/];
|
|
570
|
+
const 우리가끼운것 = (글) => 우리글.some((r) => r.test(String(글).trim()));
|
|
571
|
+
const m = 쓸만한.find((x) => !통째괄호(x.content))
|
|
572
|
+
?? 쓸만한.find((x) => !우리가끼운것(x.content))
|
|
573
|
+
?? null;
|
|
396
574
|
return m ? m.content.replace(/\s+/g, ' ').trim() : '(빈 대화)';
|
|
397
575
|
}
|
|
398
576
|
|
|
@@ -461,7 +639,22 @@ function 한줄(root, c) {
|
|
|
461
639
|
return {
|
|
462
640
|
...c,
|
|
463
641
|
model: meta?.model ?? '?',
|
|
464
|
-
|
|
642
|
+
/*
|
|
643
|
+
* 「몇 번 오갔나」 는 **사람이 몇 번 말을 걸었나** 다.
|
|
644
|
+
*
|
|
645
|
+
* 어제 세 가지를 물어본 대화가 목록에 `43번 오감` 으로 떠 있었다. 사람은
|
|
646
|
+
* 그 숫자로 「어느 것이 그 긴 대화였나」 를 고르는데, 도구를 많이 쓴 짧은
|
|
647
|
+
* 대화가 언제나 제일 길어 보였다 — 고르라고 적어 둔 숫자가 거꾸로 가리킨 것이다.
|
|
648
|
+
*
|
|
649
|
+
* 까닭은 `role` 만 본 것이다. Anthropic 규격에는 도구 차례가 따로 없어서
|
|
650
|
+
* 결과가 **사람 차례**에 실려 온다(backend/adapter.js 의 toolMessage).
|
|
651
|
+
* 그래서 도구 결과 한 건이 그대로 사람 한 마디로 세어졌다. 바로 위
|
|
652
|
+
* firstAsk 는 이미 글인지를 보고 있었는데, 여기만 옛 모양으로 남아 있었다.
|
|
653
|
+
*
|
|
654
|
+
* 규격마다 다른 자리를 보는 것은 우리 몫이 아니다 — 그 판단은 adapter.js
|
|
655
|
+
* 한 곳에만 둔다. session.js·compact.js 가 같은 함정을 같은 함수로 닫았다.
|
|
656
|
+
*/
|
|
657
|
+
turns: messages.filter((m) => m.role === 'user' && !도구결과인가(m)).length,
|
|
465
658
|
messages: messages.length,
|
|
466
659
|
first: firstAsk(messages),
|
|
467
660
|
};
|
|
@@ -496,8 +689,13 @@ export function latest(root) {
|
|
|
496
689
|
* `list(root, { limit: 20 })` 로 스무 개를 통째로 읽으면 안 된다 — 이 파일이
|
|
497
690
|
* 위에서 경고한 그 자리다. 대화 파일 하나가 수 MB 이고, 이건 켤 때마다 돈다.
|
|
498
691
|
* 보통은 첫 파일 하나만 읽고 끝난다.
|
|
692
|
+
*
|
|
693
|
+
* **개수로는 끊지 않는다** (2.0.0 6회차 · Gemini 저장6w-b2 Y4). 20 에서 끊었더니, 터미널을
|
|
694
|
+
* 켜자마자 · 에디터가 새 대화 창을 열 때마다 적히는 머리글만 있는 파일이 스물 넘게 쌓이면
|
|
695
|
+
* 어제 대화가 그대로 있는데 --continue 가 「이어갈 대화가 없다」 가 됐다. 그런 파일은
|
|
696
|
+
* 몇십 바이트라 끝까지 훑어도 싸고, 성한 것을 만나면 거기서 멈춘다.
|
|
499
697
|
*/
|
|
500
|
-
for (const c of list(root, { limit:
|
|
698
|
+
for (const c of list(root, { limit: Infinity, 속까지: false })) {
|
|
501
699
|
const 줄 = 한줄(root, c);
|
|
502
700
|
if (줄 && !줄.못읽음) return 줄;
|
|
503
701
|
}
|
|
@@ -508,7 +706,7 @@ export function latest(root) {
|
|
|
508
706
|
* 오래된 것은 정리한다. 안 그러면 폴더가 끝없이 자란다.
|
|
509
707
|
* 최근 keep 개는 무조건 남기고, 그보다 오래되고 days 를 넘긴 것만 지운다.
|
|
510
708
|
*/
|
|
511
|
-
export function prune(root, { keep = 30, days = 30 } = {}) {
|
|
709
|
+
export function prune(root, { keep = 30, days = 30, 남길것 = [] } = {}) {
|
|
512
710
|
/*
|
|
513
711
|
* 지울지 말지는 파일 시각만 보면 안다. 내용은 필요 없다 —
|
|
514
712
|
* 여기서 전부 읽던 것이 켤 때 몇 초씩 멈추던 원인이었다.
|
|
@@ -517,8 +715,20 @@ export function prune(root, { keep = 30, days = 30 } = {}) {
|
|
|
517
715
|
* 영영 안 지워진다. 폴더가 끝없이 자라지 말라고 있는 함수가 딱 그 일을
|
|
518
716
|
* 못 하게 된다. 열지도 않고 시각만 보는 목록이라 전부 봐도 싸다.
|
|
519
717
|
*/
|
|
718
|
+
/*
|
|
719
|
+
* ── 지금 쓰는 대화는 시각과 상관없이 남긴다 (사냥5 H5-2) ─────────────────
|
|
720
|
+
*
|
|
721
|
+
* 한 달 넘은 대화를 --resume · session/load 로 이어받으면, 그 파일은 **아직 한 줄도
|
|
722
|
+
* 안 늘어서** 시각이 한 달 전 그대로다. 그리고 두 문 다 이어받기 바로 뒤에 여기를
|
|
723
|
+
* 부른다. 그래서 방금 이어받은 파일이 지워졌고, 다음 한 줄은 머리글도 옛 대화도
|
|
724
|
+
* 없는 새 파일에 적혔다 — 화면에는 「이어 받았습니다」 가 떠 있는데. 부르는 쪽이
|
|
725
|
+
* 열어 둔 대화 이름을 넘기면 그것은 안 지운다.
|
|
726
|
+
*/
|
|
727
|
+
const 지킬것 = new Set([...(남길것 ?? [])].map(String));
|
|
520
728
|
const all = list(root, { limit: Infinity, 속까지: false });
|
|
521
|
-
const 자를것 = all.slice(keep)
|
|
729
|
+
const 자를것 = all.slice(keep)
|
|
730
|
+
.filter((s) => !지킬것.has(s.id))
|
|
731
|
+
.filter((s) => (Date.now() - s.at.getTime()) > days * 86400000);
|
|
522
732
|
// 지웠다고 세는 것은 **정말 지워진 것**만이다.
|
|
523
733
|
let 지운수 = 0;
|
|
524
734
|
for (const s of 자를것) { try { rmSync(s.file, { force: true }); 지운수 += 1; } catch { /* 잡혀 있으면 다음에 */ } }
|
package/src/agent/threads.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
/** 이름이 없을 때 붙여 줄 이름. 사람이 목록에서 알아볼 수 있으면 된다. */
|
|
30
30
|
import { 파일기억 } from './filemem.js';
|
|
31
|
+
import { 규격맞추기 } from './session.js';
|
|
31
32
|
|
|
32
33
|
function 기본이름(n) { return `갈래 ${n}`; }
|
|
33
34
|
|
|
@@ -42,6 +43,20 @@ export class Threads {
|
|
|
42
43
|
this.session = session;
|
|
43
44
|
this.ctx = ctx;
|
|
44
45
|
this.새store = 새store;
|
|
46
|
+
/*
|
|
47
|
+
* 이어받은 대화를 **지금 규격으로** 옮겨 적는다 (agent/session.js 의 규격맞추기).
|
|
48
|
+
*
|
|
49
|
+
* 저장 파일 머리글에는 규격이 없다. 어제 Anthropic 으로 한 대화를 오늘 OpenAI
|
|
50
|
+
* 호환 연결로 --resume 하면 tool_use·tool_result 블록이 그대로 나가서 첫 마디가
|
|
51
|
+
* 400 이었다. 이어받은 대화가 들어오는 자리가 여기(첫 한마디 전, 갈래 표에
|
|
52
|
+
* 담기 전)다. 턴마다 적는 자리(repl 의 saved)가 메시지 **수**로 세므로, 수가
|
|
53
|
+
* 바뀔 수 있는 이 손질은 턴이 돌기 전에 끝나야 한다 — 그래서 여기다.
|
|
54
|
+
* 이미 맞는 대화면 아무것도 안 바뀐다.
|
|
55
|
+
*/
|
|
56
|
+
const 옮김 = 규격맞추기(session?.messages, session?.conn?.kind);
|
|
57
|
+
if (옮김.바꾼것) session.messages = 옮김.messages;
|
|
58
|
+
/** 이어받을 때 옮겨 적은 것. 화면이 말할 수 있게 남긴다. */
|
|
59
|
+
this.옮긴것 = 옮김.바꾼것 ? { 바꾼것: 옮김.바꾼것, 뺀것: 옮김.뺀것 } : null;
|
|
45
60
|
/*
|
|
46
61
|
* 남은 할 일과 시킨 말 원문을 저장 파일과 묶는다 (agent/store.js 의 살림따라가기).
|
|
47
62
|
*
|
|
@@ -171,7 +186,7 @@ export class Threads {
|
|
|
171
186
|
this.센것++;
|
|
172
187
|
const store = this.새store();
|
|
173
188
|
const g = {
|
|
174
|
-
이름:
|
|
189
|
+
이름: this.#새이름(이름),
|
|
175
190
|
messages: 물려줄것 ? [...물려줄것] : [],
|
|
176
191
|
// session.usage 와 **같은 모양**이어야 한다. 여태 다섯 칸만 있어서
|
|
177
192
|
// 갈래에서는 캐시·생각·못잰것 칸이 아예 없었다. 더하는 자리들이 전부
|
|
@@ -199,6 +214,25 @@ export class Threads {
|
|
|
199
214
|
return this.#꺼내기(this.갈래들.length - 1);
|
|
200
215
|
}
|
|
201
216
|
|
|
217
|
+
/*
|
|
218
|
+
* ── 이름이 번호와도, 다른 갈래 이름과도 안 겹치게 (2.0.0 6회차 · Gemini 스레드6) ──
|
|
219
|
+
*
|
|
220
|
+
* 찾기() 는 숫자뿐인 말을 **번호로 먼저** 읽는다. 그래서 「2」 라는 이름의 갈래는 이름으로는
|
|
221
|
+
* 영영 못 갔고, `/thread 2` 는 둘째 갈래로 갔다. 같은 이름을 두 번 주면 찾기가 늘 첫째를
|
|
222
|
+
* 집어 둘째는 번호로만 갈 수 있었다. 찾기 규칙을 바꾸면 이미 쓰는 번호 길이 흔들리므로,
|
|
223
|
+
* 만드는 자리에서 겹치지 않는 이름을 준다 — 숫자뿐이면 앞에 `#`, 겹치면 뒤에 ` (2)`.
|
|
224
|
+
* 새로() 가 돌려주는 갈래의 이름을 화면이 그대로 보여 주므로 사람은 바뀐 이름을 본다.
|
|
225
|
+
*/
|
|
226
|
+
#새이름(이름) {
|
|
227
|
+
let s = String(이름 ?? '').trim() || 기본이름(this.센것);
|
|
228
|
+
if (/^\d+$/.test(s)) s = `#${s}`;
|
|
229
|
+
const 겹치나 = (x) => this.갈래들.some((g) => g.이름.toLowerCase() === x.toLowerCase());
|
|
230
|
+
if (!겹치나(s)) return s;
|
|
231
|
+
let k = 2;
|
|
232
|
+
while (겹치나(`${s} (${k})`)) k++;
|
|
233
|
+
return `${s} (${k})`;
|
|
234
|
+
}
|
|
235
|
+
|
|
202
236
|
/** 지금 대화를 그대로 복사해 새 갈래로 나간다. */
|
|
203
237
|
갈라내기(이름 = '') {
|
|
204
238
|
return this.새로(이름, this.session.messages);
|
|
@@ -211,9 +245,14 @@ export class Threads {
|
|
|
211
245
|
찾기(말) {
|
|
212
246
|
const s = String(말 ?? '').trim();
|
|
213
247
|
if (!s) return -1;
|
|
248
|
+
/*
|
|
249
|
+
* 범위 밖 숫자는 **번호가 아니라 이름**이다 (2.0.0 8회차 스키마). 여기서 -1 로 끝냈더니
|
|
250
|
+
* 위 #새이름() 이 `#404` 로 바꿔 준 갈래를, 목록에 보이는 그 숫자로는 영영 못 갔다.
|
|
251
|
+
* 범위 안 번호는 그대로 번호가 이긴다 — 6회차 스레드6 이 정한 차례를 안 흔든다.
|
|
252
|
+
*/
|
|
214
253
|
if (/^\d+$/.test(s)) {
|
|
215
254
|
const i = Number(s) - 1;
|
|
216
|
-
|
|
255
|
+
if (i >= 0 && i < this.갈래들.length) return i;
|
|
217
256
|
}
|
|
218
257
|
const 낮 = s.toLowerCase();
|
|
219
258
|
const 딱 = this.갈래들.findIndex((g) => g.이름.toLowerCase() === 낮);
|
|
@@ -237,7 +276,17 @@ export class Threads {
|
|
|
237
276
|
if (this.갈래들.length <= 1) return { ok: false, why: '갈래가 하나뿐입니다.' };
|
|
238
277
|
const i = 말 ? this.찾기(말) : this.자리;
|
|
239
278
|
if (i < 0) return { ok: false, why: '그런 갈래가 없습니다.' };
|
|
240
|
-
|
|
279
|
+
/*
|
|
280
|
+
* **어느 갈래를 닫든** 먼저 담아 둔다.
|
|
281
|
+
*
|
|
282
|
+
* 여기가 `if (i === this.자리)` 로 지금 갈래를 닫을 때만 담았다. 다른 갈래를
|
|
283
|
+
* 닫으면 아래 #꺼내기() 가 지금 갈래를 **표에 담긴 옛 모습**으로 다시 꺼낸다 —
|
|
284
|
+
* 옮겨 온 뒤에 쌓인 말(접기가 messages 를 갈아 끼웠으면 통째로), 할 일,
|
|
285
|
+
* 시킨 말이 그 자리에서 없어지고, 다음에 적을 때 파일에 빈 할 일이 적힌다.
|
|
286
|
+
* #담아두기() 머리말이 「옮기기 전에 반드시 부른다」 인데, 닫기도 옮기기다.
|
|
287
|
+
*/
|
|
288
|
+
this.#담아두기(); // 닫는 것이 지금 갈래가 아니어도
|
|
289
|
+
|
|
241
290
|
const [닫은것] = this.갈래들.splice(i, 1);
|
|
242
291
|
// 갈래는 지워도 그 갈래가 낸 돈은 안 지워진다 (생성자의 닫힌셈).
|
|
243
292
|
for (const k of Object.keys(this.닫힌셈)) this.닫힌셈[k] += Number(닫은것.usage?.[k] ?? 0);
|