mixdog 0.9.16 → 0.9.17
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/package.json +2 -1
- package/scripts/atomic-lock-tryonce-test.mjs +66 -0
- package/scripts/bench/cache-probe-tasks.json +1 -1
- package/scripts/bench/lead-review-tasks-r3.json +1 -1
- package/scripts/bench/r4-mixed-tasks.json +1 -1
- package/scripts/bench/review-tasks.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/provider-toolcall-test.mjs +79 -2
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/src/mixdog-session-runtime.mjs +12 -0
- package/src/rules/lead/02-channels.md +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +32 -18
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +108 -30
- package/src/runtime/agent/orchestrator/session/store.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +15 -15
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -19
- package/src/runtime/memory/index.mjs +37 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -0
- package/src/runtime/shared/atomic-file.mjs +110 -0
- package/src/runtime/shared/transcript-writer.mjs +46 -4
- package/src/session-runtime/provider-models.mjs +47 -8
- package/src/tui/app/transcript-window.mjs +115 -6
- package/src/tui/app/use-transcript-window.mjs +58 -7
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/dist/index.mjs +373 -81
- package/src/tui/engine/tui-steering-persist.mjs +66 -35
- package/src/tui/engine.mjs +6 -4
- package/src/tui/index.jsx +97 -6
- package/src/ui/statusline-segments.mjs +54 -36
- package/src/ui/statusline.mjs +141 -95
|
@@ -366,6 +366,81 @@ test('anthropic(-oauth): text-only stream → no toolCalls', async () => {
|
|
|
366
366
|
assert.equal(result.content, 'hello');
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
+
test('anthropic(-oauth): thinking + signature deltas → ordered thinkingBlocks before tool_use', async () => {
|
|
370
|
+
const events = [
|
|
371
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
372
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '' } },
|
|
373
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'step ' } },
|
|
374
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'one' } },
|
|
375
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'sig123' } },
|
|
376
|
+
{ type: 'content_block_stop', index: 0 },
|
|
377
|
+
{ type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_9', name: 'shell' } },
|
|
378
|
+
{ type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"command":"ls"}' } },
|
|
379
|
+
{ type: 'content_block_stop', index: 1 },
|
|
380
|
+
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
381
|
+
{ type: 'message_stop' },
|
|
382
|
+
];
|
|
383
|
+
const result = await anthropicParseSSEStream(
|
|
384
|
+
anthropicSseResponse(events),
|
|
385
|
+
null, () => {}, () => {}, () => {}, {}, null,
|
|
386
|
+
);
|
|
387
|
+
assert.equal(result.hasThinkingContent, true);
|
|
388
|
+
assert.deepEqual(result.thinkingBlocks, [
|
|
389
|
+
{ type: 'thinking', thinking: 'step one', signature: 'sig123' },
|
|
390
|
+
]);
|
|
391
|
+
assert.deepEqual(result.toolCalls, [{ id: 'toolu_9', name: 'shell', arguments: { command: 'ls' } }]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('anthropic(-oauth): signature-only block (display omitted) → empty thinking kept with signature', async () => {
|
|
395
|
+
const events = [
|
|
396
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
397
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking' } },
|
|
398
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'sigABC' } },
|
|
399
|
+
{ type: 'content_block_stop', index: 0 },
|
|
400
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
401
|
+
{ type: 'message_stop' },
|
|
402
|
+
];
|
|
403
|
+
const result = await anthropicParseSSEStream(
|
|
404
|
+
anthropicSseResponse(events),
|
|
405
|
+
null, () => {}, () => {}, () => {}, {}, null,
|
|
406
|
+
);
|
|
407
|
+
assert.deepEqual(result.thinkingBlocks, [
|
|
408
|
+
{ type: 'thinking', thinking: '', signature: 'sigABC' },
|
|
409
|
+
]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('anthropic(-oauth): redacted_thinking round-trips exactly as {type,data} (no extra fields)', async () => {
|
|
413
|
+
const events = [
|
|
414
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
415
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'redacted_thinking', data: 'ENCRYPTED_PAYLOAD' } },
|
|
416
|
+
{ type: 'content_block_stop', index: 0 },
|
|
417
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
418
|
+
{ type: 'message_stop' },
|
|
419
|
+
];
|
|
420
|
+
const result = await anthropicParseSSEStream(
|
|
421
|
+
anthropicSseResponse(events),
|
|
422
|
+
null, () => {}, () => {}, () => {}, {}, null,
|
|
423
|
+
);
|
|
424
|
+
assert.deepEqual(result.thinkingBlocks, [
|
|
425
|
+
{ type: 'redacted_thinking', data: 'ENCRYPTED_PAYLOAD' },
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('anthropic effort: legacy claude-3-7-sonnet gets NO adaptive thinking / effort beta', () => {
|
|
430
|
+
const model = 'claude-3-7-sonnet-20250219';
|
|
431
|
+
assert.equal(modelSupportsEffort(model), false);
|
|
432
|
+
const body = _buildRequestBodyForCacheSmoke(
|
|
433
|
+
[{ role: 'user', content: 'hi' }],
|
|
434
|
+
model,
|
|
435
|
+
[],
|
|
436
|
+
{ effort: 'high' },
|
|
437
|
+
);
|
|
438
|
+
assert.equal(body.output_config, undefined);
|
|
439
|
+
// Legacy path uses the budget_tokens shape, never thinking:adaptive.
|
|
440
|
+
assert.notEqual(body.thinking?.type, 'adaptive');
|
|
441
|
+
assert.equal(shouldIncludeEffortBeta(model, { effort: 'high' }), false);
|
|
442
|
+
});
|
|
443
|
+
|
|
369
444
|
// --- Leaked tool-call recovery (shared parseSSEStream guard) ----------------
|
|
370
445
|
// The model sometimes emits a tool call as plain text tags inside text_delta
|
|
371
446
|
// instead of a native tool_use block. The guard (8th arg = known tool names)
|
|
@@ -933,7 +1008,9 @@ test('anthropic effort: sonnet-4-6 uses output_config + effort beta, not thinkin
|
|
|
933
1008
|
{ effort: 'high' },
|
|
934
1009
|
);
|
|
935
1010
|
assert.deepEqual(body.output_config, { effort: 'high' });
|
|
936
|
-
|
|
1011
|
+
// Adaptive-thinking models also carry thinking:{type:'adaptive'} — the
|
|
1012
|
+
// legacy budget_tokens shape 400s on these models.
|
|
1013
|
+
assert.deepEqual(body.thinking, { type: 'adaptive', display: 'summarized' });
|
|
937
1014
|
assert.equal(shouldIncludeEffortBeta(model, { effort: 'high' }), true);
|
|
938
1015
|
const beta = buildAnthropicBetaHeaders({ effort: true });
|
|
939
1016
|
assert.ok(beta.includes(EFFORT_BETA_HEADER));
|
|
@@ -967,7 +1044,7 @@ test('anthropic effort: xhigh on opus-4-8 is a first-class level (not downgraded
|
|
|
967
1044
|
{ effort: 'xhigh' },
|
|
968
1045
|
);
|
|
969
1046
|
assert.deepEqual(body.output_config, { effort: 'xhigh' });
|
|
970
|
-
assert.
|
|
1047
|
+
assert.deepEqual(body.thinking, { type: 'adaptive', display: 'summarized' });
|
|
971
1048
|
});
|
|
972
1049
|
|
|
973
1050
|
test('anthropic effort: explicit thinkingBudgetTokens wins over effort', () => {
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
{ "id": "uc5-yesterday", "label": "UC5 calendar yesterday", "args": { "period": "yesterday", "limit": 10 }, "expect": "browse" },
|
|
16
16
|
{ "id": "uc5-thisweek", "label": "UC5 calendar this_week", "args": { "period": "this_week", "limit": 10 }, "expect": "browse" },
|
|
17
17
|
{ "id": "uc6-category", "label": "UC6 category decision 7d", "args": { "period": "7d", "category": "decision", "limit": 10 }, "expect": "browse" }
|
|
18
|
-
]
|
|
18
|
+
]
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
-
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
-
# boot, double-init refusal, hostile-env survival.
|
|
4
|
-
#
|
|
5
|
-
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
-
|
|
7
|
-
$ErrorActionPreference = 'Stop'
|
|
8
|
-
|
|
9
|
-
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
-
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
-
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
-
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
-
$PgVer = '16.4'
|
|
14
|
-
$PgvectorVer = '0.8.2'
|
|
15
|
-
|
|
16
|
-
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
-
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
-
|
|
19
|
-
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
-
|
|
24
|
-
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
-
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
-
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
-
|
|
28
|
-
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
-
$TarPath = Join-Path $Work $Asset
|
|
30
|
-
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
-
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
-
Write-Host " sha256=$sha"
|
|
33
|
-
|
|
34
|
-
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
-
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
-
Copy-Item $TarPath $CorruptPath
|
|
37
|
-
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
-
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
-
$stream.Position = 102400
|
|
40
|
-
$stream.Write($bytes, 0, 1)
|
|
41
|
-
$stream.Close()
|
|
42
|
-
$CorruptDir = "$Work\corrupt"
|
|
43
|
-
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
-
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
-
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
-
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
-
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
-
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
-
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
-
} else {
|
|
51
|
-
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
-
$FreshDir = "$Work\fresh"
|
|
56
|
-
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
-
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
-
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
-
|
|
60
|
-
$PgBin = "$FreshDir\bin"
|
|
61
|
-
$Data = "$FreshDir\pgdata"
|
|
62
|
-
$Log = "$FreshDir\pg.log"
|
|
63
|
-
$Port = 55897
|
|
64
|
-
|
|
65
|
-
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
-
$SavedPath = $env:PATH
|
|
67
|
-
$SavedPgRoot = $env:PGROOT
|
|
68
|
-
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
-
$env:PGROOT = $null
|
|
70
|
-
$env:PGDATA = $null
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
& "$PgBin\postgres.exe" --version
|
|
74
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
-
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
-
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
-
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
-
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
-
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
-
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
-
} finally {
|
|
87
|
-
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
-
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
-
if ($rc -eq 0) {
|
|
93
|
-
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
-
} else {
|
|
95
|
-
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
-
}
|
|
97
|
-
} finally {
|
|
98
|
-
$env:PATH = $SavedPath
|
|
99
|
-
$env:PGROOT = $SavedPgRoot
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
-
}
|
|
1
|
+
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
+
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
+
# boot, double-init refusal, hostile-env survival.
|
|
4
|
+
#
|
|
5
|
+
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
|
|
9
|
+
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
+
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
+
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
+
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
+
$PgVer = '16.4'
|
|
14
|
+
$PgvectorVer = '0.8.2'
|
|
15
|
+
|
|
16
|
+
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
+
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
+
|
|
19
|
+
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
+
|
|
24
|
+
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
+
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
+
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
+
|
|
28
|
+
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
+
$TarPath = Join-Path $Work $Asset
|
|
30
|
+
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
+
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
+
Write-Host " sha256=$sha"
|
|
33
|
+
|
|
34
|
+
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
+
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
+
Copy-Item $TarPath $CorruptPath
|
|
37
|
+
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
+
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
+
$stream.Position = 102400
|
|
40
|
+
$stream.Write($bytes, 0, 1)
|
|
41
|
+
$stream.Close()
|
|
42
|
+
$CorruptDir = "$Work\corrupt"
|
|
43
|
+
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
+
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
+
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
+
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
+
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
+
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
+
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
+
} else {
|
|
51
|
+
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
+
$FreshDir = "$Work\fresh"
|
|
56
|
+
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
+
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
+
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
+
|
|
60
|
+
$PgBin = "$FreshDir\bin"
|
|
61
|
+
$Data = "$FreshDir\pgdata"
|
|
62
|
+
$Log = "$FreshDir\pg.log"
|
|
63
|
+
$Port = 55897
|
|
64
|
+
|
|
65
|
+
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
+
$SavedPath = $env:PATH
|
|
67
|
+
$SavedPgRoot = $env:PGROOT
|
|
68
|
+
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
+
$env:PGROOT = $null
|
|
70
|
+
$env:PGDATA = $null
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
& "$PgBin\postgres.exe" --version
|
|
74
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
+
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
+
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
+
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
+
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
+
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
+
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
+
} finally {
|
|
87
|
+
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
+
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
+
if ($rc -eq 0) {
|
|
93
|
+
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
+
} else {
|
|
95
|
+
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
$env:PATH = $SavedPath
|
|
99
|
+
$env:PGROOT = $SavedPgRoot
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
+
}
|
|
@@ -85,4 +85,4 @@ for (const target of ['heavy-worker', 'worker']) {
|
|
|
85
85
|
console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
|
|
86
86
|
console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
|
|
87
87
|
console.log(` same-path re-reads >=3x: ${rereads}`);
|
|
88
|
-
}
|
|
88
|
+
}
|
|
@@ -1553,6 +1553,12 @@ export async function createMixdogSessionRuntime({
|
|
|
1553
1553
|
// caller: the whole probe is detached, but it internally awaits readiness.
|
|
1554
1554
|
void (async () => {
|
|
1555
1555
|
try {
|
|
1556
|
+
// Yield one event-loop tick before the heavy chain below (module
|
|
1557
|
+
// resolve, daemon fork/health-poll) starts, so Ink's next render
|
|
1558
|
+
// (scheduled via setImmediate/timers) and any queued keypress
|
|
1559
|
+
// events get a turn first instead of being starved by this
|
|
1560
|
+
// detached chain's synchronous setup work.
|
|
1561
|
+
await new Promise((r) => setImmediate(r));
|
|
1556
1562
|
const mod = await getMemoryModule();
|
|
1557
1563
|
const started = typeof mod?.start === 'function' ? await mod.start() : null;
|
|
1558
1564
|
const port = started?.port;
|
|
@@ -1603,6 +1609,12 @@ export async function createMixdogSessionRuntime({
|
|
|
1603
1609
|
}
|
|
1604
1610
|
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
1605
1611
|
void (async () => {
|
|
1612
|
+
// Yield before the createCurrentSession/transcript/fork chain below —
|
|
1613
|
+
// same rationale as the memory-eager-init yield above: this detached
|
|
1614
|
+
// chain runs synchronous config/fs work (createCurrentSession, backend
|
|
1615
|
+
// flush, transcript writer) back-to-back, and without a tick break it
|
|
1616
|
+
// can run ahead of Ink's queued render/input handling.
|
|
1617
|
+
await new Promise((r) => setImmediate(r));
|
|
1606
1618
|
// Immediate-occupancy guarantee: make sure a session + transcript
|
|
1607
1619
|
// exist BEFORE the worker boots — a freshly-forked worker claims and
|
|
1608
1620
|
// runs transcript discovery inside its own start(), so publishing the
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# Channels
|
|
2
|
-
|
|
3
|
-
- Channel features are handled by the runtime.
|
|
1
|
+
# Channels
|
|
2
|
+
|
|
3
|
+
- Channel features are handled by the runtime.
|
|
@@ -43,6 +43,15 @@ function parseClaudeVersion(model) {
|
|
|
43
43
|
if (pair) {
|
|
44
44
|
return { family: pair[1], major: Number(pair[2]), minor: null };
|
|
45
45
|
}
|
|
46
|
+
// Legacy naming: version comes BEFORE the family (claude-3-7-sonnet,
|
|
47
|
+
// claude-3-5-sonnet, claude-3-opus). These are manual-thinking models —
|
|
48
|
+
// parse them so isLegacyAnthropicReasoningModel excludes them instead of
|
|
49
|
+
// letting modelSupportsEffort's `startsWith('claude-')` fallthrough grant
|
|
50
|
+
// them adaptive thinking + the effort beta (a hard 400).
|
|
51
|
+
const legacy = m.match(/^claude-(\d+)-(\d+)-(haiku|sonnet|opus)/);
|
|
52
|
+
if (legacy) {
|
|
53
|
+
return { family: legacy[3], major: Number(legacy[1]), minor: Number(legacy[2]) };
|
|
54
|
+
}
|
|
46
55
|
return null;
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -69,7 +78,17 @@ export function modelSupportsEffort(model) {
|
|
|
69
78
|
if (m.includes('sonnet-5') || m.includes('fable-5')) return true;
|
|
70
79
|
if (/^claude-opus-4-(6|7|8)(?:$|[-@])/.test(m)) return true;
|
|
71
80
|
if (/^claude-opus-5-/.test(m)) return true;
|
|
72
|
-
|
|
81
|
+
// Fallthrough for not-yet-enumerated modern models: only grant effort when
|
|
82
|
+
// parseClaudeVersion resolves a family with major>=4 (and not a manual-
|
|
83
|
+
// thinking legacy already excluded above). A bare `startsWith('claude-')`
|
|
84
|
+
// here would hand thinking:adaptive + the effort beta to legacy/unknown
|
|
85
|
+
// IDs (e.g. claude-3-7-sonnet) → hard 400. Unknown-shape IDs (no version
|
|
86
|
+
// parsed) fall through to false: no effort, no adaptive thinking.
|
|
87
|
+
const parsed = parseClaudeVersion(model);
|
|
88
|
+
if (parsed && (parsed.family === 'sonnet' || parsed.family === 'opus' || parsed.family === 'fable')
|
|
89
|
+
&& parsed.major >= 4) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
73
92
|
return false;
|
|
74
93
|
}
|
|
75
94
|
|
|
@@ -180,6 +199,19 @@ export function applyAnthropicEffortToBody(
|
|
|
180
199
|
: {};
|
|
181
200
|
body.output_config = { ...existing, effort: normalized };
|
|
182
201
|
}
|
|
202
|
+
// Adaptive-thinking models (4.6+) require `thinking:{type:"adaptive"}`
|
|
203
|
+
// rather than the legacy budget_tokens shape — sending
|
|
204
|
+
// `thinking:{type:"enabled"}` here 400s on sonnet-5/opus-4-7/4-8.
|
|
205
|
+
// display:"summarized" keeps reasoning blocks populated (4.7+ defaults
|
|
206
|
+
// to "omitted", silently hiding reasoning text). Gated on the same
|
|
207
|
+
// modelSupportsEffort() allowlist so older models never receive it.
|
|
208
|
+
// Set unconditionally (independent of `normalized`) so effort-capable
|
|
209
|
+
// turns always carry adaptive thinking + round-trip signatures.
|
|
210
|
+
body.thinking = { type: 'adaptive', display: 'summarized' };
|
|
211
|
+
// Adaptive/4.7+ models reject any non-default sampling param with a 400.
|
|
212
|
+
delete body.temperature;
|
|
213
|
+
delete body.top_p;
|
|
214
|
+
delete body.top_k;
|
|
183
215
|
return;
|
|
184
216
|
}
|
|
185
217
|
|
|
@@ -333,6 +333,15 @@ function toAnthropicMessages(messages) {
|
|
|
333
333
|
content = m.assistantBlocks.slice();
|
|
334
334
|
} else {
|
|
335
335
|
content = [];
|
|
336
|
+
// Adaptive-thinking round-trip: prior-turn thinking blocks are
|
|
337
|
+
// REQUIRED back, unmodified (signature intact; empty thinking
|
|
338
|
+
// field allowed), and MUST precede tool_use blocks. Emit them
|
|
339
|
+
// first, verbatim as received from the SSE parser.
|
|
340
|
+
if (Array.isArray(m.thinkingBlocks)) {
|
|
341
|
+
for (const tb of m.thinkingBlocks) {
|
|
342
|
+
if (tb && typeof tb === 'object') content.push(tb);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
336
345
|
if (m.content) content.push({ type: 'text', text: m.content });
|
|
337
346
|
for (const tc of m.toolCalls) {
|
|
338
347
|
content.push({
|
|
@@ -102,6 +102,11 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
102
102
|
let content = '';
|
|
103
103
|
let hasThinkingContent = false;
|
|
104
104
|
const contentBlockTypes = new Set();
|
|
105
|
+
// Ordered extended-thinking blocks, keyed by content_block index. Each
|
|
106
|
+
// holds the accumulated thinking text + signature exactly as received so
|
|
107
|
+
// it can be round-tripped verbatim on tool-continuation turns (required
|
|
108
|
+
// back on tool_use turns; empty thinking + signature is valid).
|
|
109
|
+
const thinkingBlocks = new Map();
|
|
105
110
|
let model = '';
|
|
106
111
|
let toolCalls = [];
|
|
107
112
|
let usage = { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, raw: null };
|
|
@@ -389,6 +394,26 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
389
394
|
inputJson: '',
|
|
390
395
|
});
|
|
391
396
|
}
|
|
397
|
+
if (block?.type === 'thinking' || block?.type === 'redacted_thinking') {
|
|
398
|
+
if (block.type === 'redacted_thinking') {
|
|
399
|
+
// Redacted blocks round-trip EXACTLY as
|
|
400
|
+
// {type:'redacted_thinking',data} — no thinking/
|
|
401
|
+
// signature fields (the API rejects the extras).
|
|
402
|
+
// `data` carries the opaque payload verbatim.
|
|
403
|
+
thinkingBlocks.set(event.index, {
|
|
404
|
+
type: 'redacted_thinking',
|
|
405
|
+
data: typeof block.data === 'string' ? block.data : '',
|
|
406
|
+
});
|
|
407
|
+
} else {
|
|
408
|
+
// Seed an ordered thinking block; deltas below
|
|
409
|
+
// append text + signature into this same slot.
|
|
410
|
+
thinkingBlocks.set(event.index, {
|
|
411
|
+
type: 'thinking',
|
|
412
|
+
thinking: typeof block.thinking === 'string' ? block.thinking : '',
|
|
413
|
+
signature: typeof block.signature === 'string' ? block.signature : '',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
392
417
|
}
|
|
393
418
|
|
|
394
419
|
if (event.type === 'content_block_delta') {
|
|
@@ -433,6 +458,21 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
433
458
|
// tool_use) can be classified by the loop as
|
|
434
459
|
// synthesis-stalled rather than silent empty.
|
|
435
460
|
hasThinkingContent = true;
|
|
461
|
+
// Accumulate the block content in order so it can be
|
|
462
|
+
// returned intact and round-tripped on the next turn.
|
|
463
|
+
// A signature_delta may arrive before any thinking_delta
|
|
464
|
+
// seeded the slot (display-omitted models emit only a
|
|
465
|
+
// signature) — lazily create it.
|
|
466
|
+
let tb = thinkingBlocks.get(event.index);
|
|
467
|
+
if (!tb) {
|
|
468
|
+
tb = { type: 'thinking', thinking: '', signature: '' };
|
|
469
|
+
thinkingBlocks.set(event.index, tb);
|
|
470
|
+
}
|
|
471
|
+
if (delta.type === 'thinking_delta') {
|
|
472
|
+
tb.thinking += delta.thinking || '';
|
|
473
|
+
} else {
|
|
474
|
+
tb.signature += delta.signature || '';
|
|
475
|
+
}
|
|
436
476
|
try { onStreamDelta?.(); } catch {}
|
|
437
477
|
}
|
|
438
478
|
if (delta?.type === 'input_json_delta') {
|
|
@@ -577,6 +617,15 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
577
617
|
stopReason,
|
|
578
618
|
hasThinkingContent,
|
|
579
619
|
contentBlockTypes: Array.from(contentBlockTypes),
|
|
620
|
+
// Ordered extended-thinking blocks (verbatim thinking text +
|
|
621
|
+
// signature) for round-tripping on tool-continuation turns. Emitted
|
|
622
|
+
// in content_block index order. Empty thinking + signature is a
|
|
623
|
+
// valid block (display-omitted models) and is kept intact.
|
|
624
|
+
thinkingBlocks: thinkingBlocks.size
|
|
625
|
+
? [...thinkingBlocks.entries()]
|
|
626
|
+
.sort((a, b) => a[0] - b[0])
|
|
627
|
+
.map(([, b]) => b)
|
|
628
|
+
: undefined,
|
|
580
629
|
};
|
|
581
630
|
} finally {
|
|
582
631
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -337,6 +337,14 @@ function toAnthropicMessages(messages) {
|
|
|
337
337
|
content = m.assistantBlocks.slice();
|
|
338
338
|
} else {
|
|
339
339
|
content = [];
|
|
340
|
+
// Adaptive-thinking round-trip: prior-turn thinking blocks are
|
|
341
|
+
// REQUIRED back, unmodified (signature intact; empty thinking
|
|
342
|
+
// field allowed), and MUST precede tool_use blocks.
|
|
343
|
+
if (Array.isArray(m.thinkingBlocks)) {
|
|
344
|
+
for (const tb of m.thinkingBlocks) {
|
|
345
|
+
if (tb && typeof tb === 'object') content.push(tb);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
340
348
|
if (m.content) content.push({ type: 'text', text: m.content });
|
|
341
349
|
for (const tc of m.toolCalls) {
|
|
342
350
|
content.push({
|
|
@@ -672,6 +680,12 @@ export class AnthropicProvider {
|
|
|
672
680
|
contentBlockTypes: Array.isArray(parseResult.contentBlockTypes)
|
|
673
681
|
? parseResult.contentBlockTypes
|
|
674
682
|
: [],
|
|
683
|
+
// Round-trip adaptive-thinking blocks (verbatim thinking +
|
|
684
|
+
// signature) so the loop can store them and replay them before
|
|
685
|
+
// tool_use on the next turn. Matches anthropic-oauth's return.
|
|
686
|
+
thinkingBlocks: Array.isArray(parseResult.thinkingBlocks) && parseResult.thinkingBlocks.length
|
|
687
|
+
? parseResult.thinkingBlocks
|
|
688
|
+
: undefined,
|
|
675
689
|
usage: {
|
|
676
690
|
inputTokens: input,
|
|
677
691
|
outputTokens: output,
|
|
@@ -81,6 +81,13 @@ export function messageEstimateText(m) {
|
|
|
81
81
|
try { text += `\n${JSON.stringify(m.toolCalls)}`; }
|
|
82
82
|
catch { text += `\n[${m.toolCalls.length} tool calls]`; }
|
|
83
83
|
}
|
|
84
|
+
// Anthropic adaptive-thinking blocks round-trip verbatim (thinking text +
|
|
85
|
+
// signature / redacted data) and are re-sent on tool-continuation turns, so
|
|
86
|
+
// they consume real input tokens. Count them or trim/compact undercounts.
|
|
87
|
+
if (m.role === 'assistant' && Array.isArray(m.thinkingBlocks) && m.thinkingBlocks.length) {
|
|
88
|
+
try { text += `\n${JSON.stringify(m.thinkingBlocks)}`; }
|
|
89
|
+
catch { text += `\n[${m.thinkingBlocks.length} thinking blocks]`; }
|
|
90
|
+
}
|
|
84
91
|
if (m.role === 'tool' && m.toolCallId) text += `\n${m.toolCallId}`;
|
|
85
92
|
return text;
|
|
86
93
|
}
|
|
@@ -1220,6 +1220,14 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1220
1220
|
// turn). Blank it so it never accumulates as input tokens.
|
|
1221
1221
|
content: suppressMidTurnText ? '' : (response.content || ''),
|
|
1222
1222
|
toolCalls: compactToolCallsForHistory(calls),
|
|
1223
|
+
// Anthropic adaptive thinking: prior-turn thinking blocks must be
|
|
1224
|
+
// returned verbatim (signature intact; empty thinking allowed) and
|
|
1225
|
+
// are REQUIRED back before tool_use blocks on tool-continuation
|
|
1226
|
+
// turns. Store them so toAnthropicMessages can build assistantBlocks
|
|
1227
|
+
// = [...thinking, tool_use...]. Other providers ignore this field.
|
|
1228
|
+
...(Array.isArray(response.thinkingBlocks) && response.thinkingBlocks.length
|
|
1229
|
+
? { thinkingBlocks: response.thinkingBlocks }
|
|
1230
|
+
: {}),
|
|
1223
1231
|
...(Array.isArray(response.reasoningItems) && response.reasoningItems.length
|
|
1224
1232
|
? { reasoningItems: response.reasoningItems }
|
|
1225
1233
|
: {}),
|