throughline 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -3
- package/README.ja.md +27 -0
- package/README.md +66 -35
- package/bin/throughline.mjs +4 -2
- package/codex/skills/throughline/SKILL.md +30 -29
- package/codex/skills/throughline/agents/openai.yaml +2 -2
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +7 -4
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +10 -10
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +19 -16
- package/docs/throughline-rollback-context-trim-insight.md +11 -7
- package/package.json +2 -2
- package/src/cli/codex-handoff-model-smoke.mjs +7 -7
- package/src/cli/codex-handoff-smoke.mjs +7 -7
- package/src/cli/codex-handoff-start.mjs +184 -10
- package/src/cli/codex-handoff-start.test.mjs +80 -1
- package/src/cli/codex-hook.mjs +37 -110
- package/src/cli/codex-hook.test.mjs +147 -32
- package/src/cli/codex-resume.mjs +7 -7
- package/src/cli/codex-summarize.mjs +7 -7
- package/src/cli/handoff-preview.mjs +6 -6
- package/src/cli/help.test.mjs +3 -1
- package/src/cli/install.mjs +42 -6
- package/src/cli/install.test.mjs +39 -5
- package/src/codex-app-server.mjs +156 -0
- package/src/codex-app-server.test.mjs +8 -0
- package/src/codex-auto-refresh.mjs +361 -29
- package/src/codex-auto-refresh.test.mjs +357 -0
- package/src/codex-thread-index.mjs +8 -2
- package/src/project-path.mjs +20 -0
- package/src/resume-context.mjs +58 -2
- package/src/resume-context.test.mjs +142 -6
- package/src/test-env.mjs +4 -0
- package/src/trim-model.mjs +6 -6
|
@@ -101,7 +101,8 @@ test('buildResumeContext: header is terse and announces the Bash invocation cont
|
|
|
101
101
|
assert.ok(!text.includes('内訳の読み方'), 'glossary block must be gone');
|
|
102
102
|
assert.ok(!text.includes('現在進行中の作業の active work context'), 'verbose framing must be gone');
|
|
103
103
|
|
|
104
|
-
// 残るのは
|
|
104
|
+
// 残るのは 3 行: 現在地参照案内 + 自然な続き + Bash 呼び出し方法
|
|
105
|
+
assert.match(text, /下の「現在地」が直前のやりとりです/);
|
|
105
106
|
assert.match(text, /直前の対話の自然な続きとして応答してください/);
|
|
106
107
|
assert.match(
|
|
107
108
|
text,
|
|
@@ -109,6 +110,132 @@ test('buildResumeContext: header is terse and announces the Bash invocation cont
|
|
|
109
110
|
);
|
|
110
111
|
});
|
|
111
112
|
|
|
113
|
+
test('buildResumeContext: 現在地 anchor surfaces the latest user/assistant exchange above L1/L2', () => {
|
|
114
|
+
const db = makeDb();
|
|
115
|
+
// 25 turns to exercise an L2 window edge and ensure the anchor picks the newest.
|
|
116
|
+
for (let t = 1; t <= 25; t += 1) {
|
|
117
|
+
insertBody(db, {
|
|
118
|
+
session: 'new',
|
|
119
|
+
origin: 'old',
|
|
120
|
+
turn: t,
|
|
121
|
+
role: 'user',
|
|
122
|
+
text: `user turn ${t}`,
|
|
123
|
+
createdAt: 1000 + t * 10,
|
|
124
|
+
});
|
|
125
|
+
insertBody(db, {
|
|
126
|
+
session: 'new',
|
|
127
|
+
origin: 'old',
|
|
128
|
+
turn: t,
|
|
129
|
+
role: 'assistant',
|
|
130
|
+
text: `assistant turn ${t}`,
|
|
131
|
+
createdAt: 1000 + t * 10 + 1,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const text = buildResumeContext(db, {
|
|
136
|
+
sessionId: 'new',
|
|
137
|
+
isInheritance: true,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assert.ok(text);
|
|
141
|
+
|
|
142
|
+
const anchorIdx = text.indexOf('### 現在地 (直前のやりとり)');
|
|
143
|
+
const l2Idx = text.indexOf('### 直前の対話 (L2 / active work thread, 古い順)');
|
|
144
|
+
assert.ok(anchorIdx > 0, '現在地 anchor section should be present');
|
|
145
|
+
assert.ok(l2Idx > anchorIdx, '現在地 anchor must appear before the L2 section');
|
|
146
|
+
|
|
147
|
+
// The anchor must point to turn 25 (the latest), not any earlier turn.
|
|
148
|
+
assert.match(text, /\*\*最新ユーザー指示\*\* \[\d\d:\d\d:\d\d\]: user turn 25$/m);
|
|
149
|
+
assert.match(text, /\*\*直前のアシスタント\*\* \[\d\d:\d\d:\d\d\]: assistant turn 25$/m);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('buildResumeContext: 現在地 anchor truncates long bodies but full body still appears in L2', () => {
|
|
153
|
+
const db = makeDb();
|
|
154
|
+
const longText = 'a'.repeat(1200);
|
|
155
|
+
insertBody(db, {
|
|
156
|
+
session: 'new',
|
|
157
|
+
origin: 'old',
|
|
158
|
+
turn: 1,
|
|
159
|
+
role: 'assistant',
|
|
160
|
+
text: longText,
|
|
161
|
+
createdAt: 1000,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const text = buildResumeContext(db, {
|
|
165
|
+
sessionId: 'new',
|
|
166
|
+
isInheritance: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
assert.ok(text);
|
|
170
|
+
|
|
171
|
+
const anchorLine = text
|
|
172
|
+
.split('\n')
|
|
173
|
+
.find((l) => l.startsWith('**直前のアシスタント**'));
|
|
174
|
+
assert.ok(anchorLine, '直前のアシスタント anchor line should be present');
|
|
175
|
+
// Anchor must be truncated with ellipsis (originally 1200 chars > 600 cap).
|
|
176
|
+
assert.ok(anchorLine.endsWith(' …'), 'long anchor body must end with the ellipsis marker');
|
|
177
|
+
assert.ok(
|
|
178
|
+
anchorLine.length < longText.length,
|
|
179
|
+
'anchor line should be shorter than the original body',
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Full body must still appear in the L2 section below.
|
|
183
|
+
assert.match(text, new RegExp(`\\[assistant\\]: ${longText}`));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('buildResumeContext: 現在地 anchor is omitted for non-inheritance sessions', () => {
|
|
187
|
+
const db = makeDb();
|
|
188
|
+
insertBody(db, {
|
|
189
|
+
session: 'new',
|
|
190
|
+
origin: 'old',
|
|
191
|
+
turn: 1,
|
|
192
|
+
role: 'assistant',
|
|
193
|
+
text: 'a body',
|
|
194
|
+
createdAt: 1000,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const text = buildResumeContext(db, {
|
|
198
|
+
sessionId: 'new',
|
|
199
|
+
isInheritance: false,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
assert.ok(text);
|
|
203
|
+
assert.ok(
|
|
204
|
+
!text.includes('現在地'),
|
|
205
|
+
'normal sessions (isInheritance=false) must not include the 現在地 anchor',
|
|
206
|
+
);
|
|
207
|
+
assert.ok(
|
|
208
|
+
!text.includes('最新ユーザー指示'),
|
|
209
|
+
'normal sessions must not surface a latest-user pointer',
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('buildResumeContext: 現在地 anchor handles a single-role recent window', () => {
|
|
214
|
+
const db = makeDb();
|
|
215
|
+
// Only user rows (no assistant) — anchor should still render with just the user line.
|
|
216
|
+
insertBody(db, {
|
|
217
|
+
session: 'new',
|
|
218
|
+
origin: 'old',
|
|
219
|
+
turn: 1,
|
|
220
|
+
role: 'user',
|
|
221
|
+
text: 'lone user message',
|
|
222
|
+
createdAt: 1000,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const text = buildResumeContext(db, {
|
|
226
|
+
sessionId: 'new',
|
|
227
|
+
isInheritance: true,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
assert.ok(text);
|
|
231
|
+
assert.ok(text.includes('### 現在地 (直前のやりとり)'));
|
|
232
|
+
assert.match(text, /\*\*最新ユーザー指示\*\* \[\d\d:\d\d:\d\d\]: lone user message/);
|
|
233
|
+
assert.ok(
|
|
234
|
+
!text.includes('**直前のアシスタント**'),
|
|
235
|
+
'no assistant body present → no 直前のアシスタント line',
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
112
239
|
test('buildResumeContext: L2 is the very last section (anchored at bottom for attention)', () => {
|
|
113
240
|
const db = makeDb();
|
|
114
241
|
insertSkeleton(db, {
|
|
@@ -239,9 +366,11 @@ test('buildResumeContext: L2 entries get inline (詳細:…) suffixes with too
|
|
|
239
366
|
|
|
240
367
|
assert.ok(text);
|
|
241
368
|
|
|
369
|
+
// Look for the L2 body line specifically (not the 現在地 anchor line which also
|
|
370
|
+
// contains the latest assistant body).
|
|
242
371
|
const turnWithToolsLine = text
|
|
243
372
|
.split('\n')
|
|
244
|
-
.find((l) =>
|
|
373
|
+
.find((l) => /^\[\d\d:\d\d:\d\d\] \[assistant\]: turn with tools/.test(l));
|
|
245
374
|
assert.ok(turnWithToolsLine, 'L2 line for turn 5 should exist');
|
|
246
375
|
// - tool_input + tool_output は tool 名で集約 (Bash ×2)
|
|
247
376
|
// - hook 出力 (system) は suffix から除外
|
|
@@ -265,7 +394,9 @@ test('buildResumeContext: L2 entries get inline (詳細:…) suffixes with too
|
|
|
265
394
|
'per-line should not repeat the throughline detail command (the header announces it)',
|
|
266
395
|
);
|
|
267
396
|
|
|
268
|
-
const plainLine = text
|
|
397
|
+
const plainLine = text
|
|
398
|
+
.split('\n')
|
|
399
|
+
.find((l) => /^\[\d\d:\d\d:\d\d\] \[user\]: plain user message/.test(l));
|
|
269
400
|
assert.ok(plainLine, 'L2 line for turn 6 should exist');
|
|
270
401
|
assert.ok(
|
|
271
402
|
!plainLine.includes('詳細:'),
|
|
@@ -341,9 +472,14 @@ test('buildResumeContext: (詳細:…) suffix appears only on the last role ro
|
|
|
341
472
|
|
|
342
473
|
assert.ok(text);
|
|
343
474
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
475
|
+
// Match L2 body lines specifically (`[HH:MM:SS] [role]: ...`) to avoid colliding
|
|
476
|
+
// with the 現在地 anchor lines (`**最新ユーザー指示** [HH:MM:SS]: ...`).
|
|
477
|
+
const lines = text.split('\n');
|
|
478
|
+
const userTurn5 = lines.find((l) => /^\[\d\d:\d\d:\d\d\] \[user\]: user side of turn 5/.test(l));
|
|
479
|
+
const assistantTurn5 = lines.find(
|
|
480
|
+
(l) => /^\[\d\d:\d\d:\d\d\] \[assistant\]: assistant side of turn 5/.test(l),
|
|
481
|
+
);
|
|
482
|
+
const userTurn6 = lines.find((l) => /^\[\d\d:\d\d:\d\d\] \[user\]: lone user turn/.test(l));
|
|
347
483
|
|
|
348
484
|
assert.ok(userTurn5 && assistantTurn5 && userTurn6);
|
|
349
485
|
// Turn 5: only assistant (last role of the turn) gets the suffix
|
package/src/test-env.mjs
ADDED
package/src/trim-model.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { inspectCodexPlannedRollbackRestoreSafety } from './codex-rollout-memory.mjs';
|
|
2
2
|
import { buildHandoffRecord, N_RECENT_L2 } from './handoff-record.mjs';
|
|
3
|
+
import { sameProjectPath } from './project-path.mjs';
|
|
3
4
|
import { estimateTokens } from './token-estimator.mjs';
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_TRIM_KEEP_RECENT = N_RECENT_L2;
|
|
@@ -31,15 +32,14 @@ function loadSession(db, sessionId) {
|
|
|
31
32
|
export function findLatestSessionIdForProject(db, projectPath) {
|
|
32
33
|
if (!projectPath) return null;
|
|
33
34
|
try {
|
|
34
|
-
const
|
|
35
|
+
const rows = db
|
|
35
36
|
.prepare(
|
|
36
|
-
`SELECT session_id
|
|
37
|
+
`SELECT session_id, project_path
|
|
37
38
|
FROM sessions
|
|
38
|
-
|
|
39
|
-
ORDER BY updated_at DESC
|
|
40
|
-
LIMIT 1`,
|
|
39
|
+
ORDER BY updated_at DESC`,
|
|
41
40
|
)
|
|
42
|
-
.
|
|
41
|
+
.all();
|
|
42
|
+
const row = rows.find((candidate) => sameProjectPath(candidate.project_path, projectPath));
|
|
43
43
|
return row?.session_id ?? null;
|
|
44
44
|
} catch {
|
|
45
45
|
return null;
|