mixdog 0.9.24 → 0.9.26
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/build-runtime-windows.ps1 +242 -242
- package/scripts/explore-bench-tmp.mjs +1 -1
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/steering-fold-provenance-test.mjs +71 -0
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/scripts/webhook-smoke.mjs +46 -53
- package/src/defaults/cycle3-review-prompt.md +11 -4
- package/src/defaults/memory-promote-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +6 -1
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/shared/01-tool.md +8 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +17 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +51 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +164 -9
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +45 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +7 -3
- package/src/runtime/channels/backends/discord.mjs +10 -3
- package/src/runtime/channels/lib/config.mjs +13 -11
- package/src/runtime/channels/lib/inbound-handler.mjs +45 -1
- package/src/runtime/channels/lib/memory-client.mjs +22 -2
- package/src/runtime/channels/lib/owned-runtime.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +226 -208
- package/src/runtime/channels/lib/status-snapshot.mjs +16 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +7 -318
- package/src/runtime/channels/lib/webhook.mjs +98 -150
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/memory/index.mjs +50 -25
- package/src/runtime/memory/lib/cycle-scheduler.mjs +20 -2
- package/src/runtime/memory/lib/memory-config-flags.mjs +13 -1
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +59 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +10 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +79 -9
- package/src/runtime/memory/lib/pg/adapter.mjs +6 -1
- package/src/runtime/memory/lib/pg/process.mjs +19 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +125 -14
- package/src/runtime/memory/lib/query-handlers.mjs +102 -0
- package/src/runtime/memory/lib/recall-format.mjs +60 -22
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +20 -6
- package/src/runtime/memory/tool-defs.mjs +1 -1
- package/src/runtime/shared/child-guardian.mjs +2 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/schedules-db.mjs +350 -0
- package/src/runtime/shared/service-discovery.mjs +169 -0
- package/src/runtime/shared/spawn-flags.mjs +27 -0
- package/src/runtime/shared/tool-primitives.mjs +3 -1
- package/src/runtime/shared/tool-surface.mjs +19 -3
- package/src/runtime/shared/update-checker.mjs +54 -10
- package/src/runtime/shared/webhooks-db.mjs +405 -0
- package/src/session-runtime/channel-config-api.mjs +13 -13
- package/src/session-runtime/lifecycle-api.mjs +14 -0
- package/src/session-runtime/runtime-core.mjs +39 -20
- package/src/standalone/agent-tool.mjs +42 -11
- package/src/standalone/channel-admin.mjs +173 -121
- package/src/standalone/channel-worker.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +10 -5
- package/src/tui/App.jsx +32 -10
- package/src/tui/app/channel-pickers.mjs +9 -9
- package/src/tui/app/clipboard.mjs +14 -0
- package/src/tui/app/doctor.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +17 -7
- package/src/tui/app/settings-picker.mjs +2 -2
- package/src/tui/app/transcript-window.mjs +37 -1
- package/src/tui/app/use-mouse-input.mjs +19 -6
- package/src/tui/components/ToolExecution.jsx +12 -4
- package/src/tui/display-width.mjs +10 -4
- package/src/tui/dist/index.mjs +166 -58
- package/src/tui/engine/agent-job-feed.mjs +42 -3
- package/src/tui/engine/session-api-ext.mjs +12 -12
- package/src/tui/index.jsx +12 -2
- package/src/ui/statusline-segments.mjs +12 -1
- package/src/workflows/default/WORKFLOW.md +29 -38
- package/src/workflows/solo/WORKFLOW.md +15 -20
- package/vendor/ink/build/display-width.js +5 -4
- package/src/runtime/shared/schedules-store.mjs +0 -82
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { foldUserTextIntoToolResultTail } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
|
|
4
|
+
import { _buildRequestBodyForCacheSmoke } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
5
|
+
|
|
6
|
+
// A tool_result turn followed by a plain user text turn.
|
|
7
|
+
function baseTranscript(userMeta) {
|
|
8
|
+
return [
|
|
9
|
+
{ role: 'user', content: 'do the thing' },
|
|
10
|
+
{ role: 'assistant', content: '', toolCalls: [{ id: 't1', name: 'read', arguments: {} }] },
|
|
11
|
+
{ role: 'tool', toolCallId: 't1', content: 'file contents here' },
|
|
12
|
+
{ role: 'user', content: 'actually stop and do X instead', ...(userMeta ? { meta: userMeta } : {}) },
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('plain user text after tool_result is folded into the tool_result turn', () => {
|
|
17
|
+
const body = _buildRequestBodyForCacheSmoke(baseTranscript(null), 'claude-sonnet-4');
|
|
18
|
+
const msgs = body.messages;
|
|
19
|
+
const lastUser = msgs[msgs.length - 1];
|
|
20
|
+
assert.equal(lastUser.role, 'user');
|
|
21
|
+
const hasToolResult = lastUser.content.some((b) => b.type === 'tool_result');
|
|
22
|
+
assert.ok(hasToolResult, 'trailing user turn carries the tool_result');
|
|
23
|
+
assert.ok(JSON.stringify(lastUser.content).includes('do X instead'), 'plain text folded into tool_result');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('steering-tagged user text after tool_result stays a separate user turn', () => {
|
|
27
|
+
const body = _buildRequestBodyForCacheSmoke(baseTranscript({ source: 'steering' }), 'claude-sonnet-4');
|
|
28
|
+
const msgs = body.messages;
|
|
29
|
+
const lastUser = msgs[msgs.length - 1];
|
|
30
|
+
assert.equal(lastUser.role, 'user', 'steering message is its own user turn');
|
|
31
|
+
const isToolResultTurn = Array.isArray(lastUser.content)
|
|
32
|
+
&& lastUser.content.some((b) => b.type === 'tool_result');
|
|
33
|
+
assert.equal(isToolResultTurn, false, 'steering turn is not a tool_result turn');
|
|
34
|
+
assert.ok(JSON.stringify(lastUser.content).includes('do X instead'), 'steering text preserved as user input');
|
|
35
|
+
const prev = msgs[msgs.length - 2];
|
|
36
|
+
assert.ok(
|
|
37
|
+
Array.isArray(prev.content) && prev.content.some((b) => b.type === 'tool_result'),
|
|
38
|
+
'tool_result stays on its own preceding turn',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('foldUserTextIntoToolResultTail unit: folds plain text tail', () => {
|
|
43
|
+
const result = [{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: 'r' }] }];
|
|
44
|
+
assert.equal(foldUserTextIntoToolResultTail(result, 'hi'), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// --- API-key Anthropic provider (anthropic.mjs) path ---
|
|
48
|
+
import { _toAnthropicMessagesForTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
|
|
49
|
+
|
|
50
|
+
test('anthropic.mjs: plain user text after tool_result is folded', () => {
|
|
51
|
+
const msgs = _toAnthropicMessagesForTest(baseTranscript(null));
|
|
52
|
+
const lastUser = msgs[msgs.length - 1];
|
|
53
|
+
assert.equal(lastUser.role, 'user');
|
|
54
|
+
assert.ok(lastUser.content.some((b) => b.type === 'tool_result'), 'trailing turn carries tool_result');
|
|
55
|
+
assert.ok(JSON.stringify(lastUser.content).includes('do X instead'), 'plain text folded into tool_result');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('anthropic.mjs: steering-tagged user text stays a separate user turn', () => {
|
|
59
|
+
const msgs = _toAnthropicMessagesForTest(baseTranscript({ source: 'steering' }));
|
|
60
|
+
const lastUser = msgs[msgs.length - 1];
|
|
61
|
+
assert.equal(lastUser.role, 'user', 'steering message is its own user turn');
|
|
62
|
+
const isToolResultTurn = Array.isArray(lastUser.content)
|
|
63
|
+
&& lastUser.content.some((b) => b.type === 'tool_result');
|
|
64
|
+
assert.equal(isToolResultTurn, false, 'steering turn is not a tool_result turn');
|
|
65
|
+
assert.ok(JSON.stringify(lastUser.content).includes('do X instead'), 'steering text preserved as user input');
|
|
66
|
+
const prev = msgs[msgs.length - 2];
|
|
67
|
+
assert.ok(
|
|
68
|
+
Array.isArray(prev.content) && prev.content.some((b) => b.type === 'tool_result'),
|
|
69
|
+
'tool_result stays on its own preceding turn',
|
|
70
|
+
);
|
|
71
|
+
});
|
|
@@ -88,4 +88,4 @@ for (const target of ['heavy-worker', 'worker']) {
|
|
|
88
88
|
console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
|
|
89
89
|
console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
|
|
90
90
|
console.log(` same-path re-reads >=3x: ${rereads}`);
|
|
91
|
-
}
|
|
91
|
+
}
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import * as crypto from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { mkdtempSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { readMarkdownDocument } from '../src/runtime/shared/markdown-frontmatter.mjs';
|
|
8
8
|
|
|
9
9
|
// DATA_DIR resolves from MIXDOG_DATA_DIR at module import time (via
|
|
10
10
|
// ./config.mjs -> resolvePluginData), so the env var MUST be set before
|
|
11
|
-
// the first dynamic import of webhook.mjs.
|
|
11
|
+
// the first dynamic import of webhook.mjs. Endpoint defs + delivery dedup now
|
|
12
|
+
// live in PG; this smoke suite deliberately exercises ONLY the pure,
|
|
13
|
+
// PG-free surface (signature verify + header helpers) so it needs no live DB.
|
|
12
14
|
const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-webhook-test-'));
|
|
13
15
|
process.env.MIXDOG_DATA_DIR = dataDir;
|
|
14
16
|
|
|
15
17
|
const {
|
|
16
18
|
extractSignature,
|
|
17
19
|
verifySignature,
|
|
18
|
-
loadEndpointConfig,
|
|
19
20
|
STRIPE_TOLERANCE_MS,
|
|
20
21
|
} = await import('../src/runtime/channels/lib/webhook.mjs');
|
|
22
|
+
const { extractDeliveryId, buildHeadersSummary } = await import(
|
|
23
|
+
'../src/runtime/channels/lib/webhook/deliveries.mjs'
|
|
24
|
+
);
|
|
21
25
|
|
|
22
26
|
function hmacHex(secret, payload) {
|
|
23
27
|
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
@@ -141,56 +145,45 @@ test('extractSignature: returns null when no known signature header is present',
|
|
|
141
145
|
assert.equal(extractSignature(headers, 'github'), null);
|
|
142
146
|
});
|
|
143
147
|
|
|
144
|
-
// ──
|
|
145
|
-
|
|
146
|
-
test('
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
assert.equal(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test('loadEndpointConfig: returns null for a name with no WEBHOOK.md', () => {
|
|
186
|
-
assert.equal(loadEndpointConfig('never-registered'), null);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// loadEndpointConfig only surfaces the frontmatter (by design — see
|
|
190
|
-
// webhook.mjs comment above loadEndpointConfig); confirm the underlying
|
|
191
|
-
// shared parser it delegates to also recovers the markdown body, since
|
|
192
|
-
// the loader's frontmatter/body split is not independently exercised
|
|
193
|
-
// through the exported loadEndpointConfig surface.
|
|
148
|
+
// ── delivery-id extraction: header precedence + null fallback ─────────
|
|
149
|
+
|
|
150
|
+
test('extractDeliveryId: prefers x-github-delivery over other id headers', () => {
|
|
151
|
+
const headers = { 'x-github-delivery': 'gh-1', 'x-delivery-id': 'dl-1', 'x-request-id': 'rq-1' };
|
|
152
|
+
assert.equal(extractDeliveryId(headers), 'gh-1');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('extractDeliveryId: falls back to x-delivery-id then x-request-id', () => {
|
|
156
|
+
assert.equal(extractDeliveryId({ 'x-delivery-id': 'dl-1', 'x-request-id': 'rq-1' }), 'dl-1');
|
|
157
|
+
assert.equal(extractDeliveryId({ 'x-request-id': 'rq-1' }), 'rq-1');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('extractDeliveryId: returns null when no id header is present', () => {
|
|
161
|
+
assert.equal(extractDeliveryId({ 'content-type': 'application/json' }), null);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── headers summary: event/delivery/content-type + signature presence ─
|
|
165
|
+
|
|
166
|
+
test('buildHeadersSummary: captures event, delivery, content-type and signature presence', () => {
|
|
167
|
+
const summary = buildHeadersSummary({
|
|
168
|
+
'x-github-event': 'issues',
|
|
169
|
+
'x-github-delivery': 'gh-1',
|
|
170
|
+
'x-hub-signature-256': 'sha256=abc',
|
|
171
|
+
'content-type': 'application/json',
|
|
172
|
+
});
|
|
173
|
+
assert.equal(summary.event_type, 'issues');
|
|
174
|
+
assert.equal(summary.delivery_id, 'gh-1');
|
|
175
|
+
assert.equal(summary.signature_present, true);
|
|
176
|
+
assert.equal(summary.content_type, 'application/json');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('buildHeadersSummary: signature_present is false when no signature header is present', () => {
|
|
180
|
+
const summary = buildHeadersSummary({ 'x-github-event': 'push' });
|
|
181
|
+
assert.equal(summary.signature_present, false);
|
|
182
|
+
assert.equal(summary.event_type, 'push');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// The webhook body/instructions still round-trip through the shared markdown
|
|
186
|
+
// parser (frontmatter + body split); confirm that pure surface directly.
|
|
194
187
|
test('readMarkdownDocument: WEBHOOK.md-shaped input yields frontmatter + body', () => {
|
|
195
188
|
const raw = [
|
|
196
189
|
'---',
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
You review user-curated CORE memory against the current project memory. Each
|
|
4
4
|
entry is shown with its most-related current memory. Emit ONE verdict line per
|
|
5
5
|
entry id. The system may conservatively apply safe compression updates and
|
|
6
|
-
strict duplicate merges automatically
|
|
7
|
-
|
|
6
|
+
strict duplicate merges automatically, and now also auto-applies deletes you
|
|
7
|
+
tag as clear junk (see the delete reasons below), capped per run for safety;
|
|
8
|
+
non-junk or unreasoned deletes and broad rewrites still require explicit user
|
|
9
|
+
confirmation. Extracting a lesson from a narrative is a BROAD
|
|
8
10
|
rewrite — emit it only as a proposal (proposal mode), never as an auto-applied
|
|
9
11
|
conservative "safe compression" update; conservative mode must not lossy-rewrite
|
|
10
12
|
user-curated core. The first character of your response is a digit.
|
|
@@ -64,7 +66,12 @@ or inconclusive, keep the CORE entry.
|
|
|
64
66
|
so a CORE copy is redundant); OR is sourced from code, rules files, or skill
|
|
65
67
|
docs, or is an implementation spec, constant, measurement, resolved-bug
|
|
66
68
|
story, or status snapshot that fits no L1/L2/L3 layer. Do not delete an entry
|
|
67
|
-
that adds durable specifics the rule itself does not state.
|
|
69
|
+
that adds durable specifics the rule itself does not state. ALWAYS append a
|
|
70
|
+
reason tag: `<id>|delete|<reason>` where `<reason>` is one of `duplicate`,
|
|
71
|
+
`default` (restates a built-in/default rule), `restatement`, `obsolete`,
|
|
72
|
+
`implemented`, `resolved`, `stale`, `past_event`, or `log`. A bare
|
|
73
|
+
`<id>|delete` with no reason, or any reason outside that set, is treated as
|
|
74
|
+
"needs confirmation" and only removed on an explicit APPLY CYCLE3 run.
|
|
68
75
|
|
|
69
76
|
A verbose durable entry is always `update`, never `keep`.
|
|
70
77
|
Delete is the rarest verdict. Prefer `keep` for durable rules/preferences and
|
|
@@ -97,7 +104,7 @@ One line per entry id, any order:
|
|
|
97
104
|
<id>|update|<element>|<summary>
|
|
98
105
|
<id>|merge|<target_id>|<source_ids_csv>
|
|
99
106
|
<id>|superseded|<newer_id>
|
|
100
|
-
<id>|delete
|
|
107
|
+
<id>|delete|<reason>
|
|
101
108
|
```
|
|
102
109
|
|
|
103
110
|
`summary` ≤120 chars, one clause. No literal `|` or newline inside a field
|
|
@@ -112,6 +112,15 @@ A/B pending rows that encode lasting behavior or map anchors; transient
|
|
|
112
112
|
When `Active > cap`, contract strictly: any active entry without a concrete
|
|
113
113
|
A/B reason must archive.
|
|
114
114
|
|
|
115
|
+
**Structural block (enforced, not advisory).** Pending `task`/`issue` rows and
|
|
116
|
+
any row that reads as status/review churn or a benchmark/measurement result
|
|
117
|
+
snapshot are blocked from promotion by the pipeline itself — an `active` verdict
|
|
118
|
+
on them is dropped and the row is **held pending and re-judged on a later
|
|
119
|
+
cycle** (it is neither promoted nor force-archived here). Do NOT archive such a
|
|
120
|
+
row just because it cannot promote now: leave it to be re-confirmed, or promote
|
|
121
|
+
the durable L2 lesson distilled from it under a durable category. Durable
|
|
122
|
+
rules/preferences/constraints promote normally.
|
|
123
|
+
|
|
115
124
|
If useful content is buried inside work narrative, keep only the durable L2
|
|
116
125
|
behavior lesson (via `update` on an active row, or `active` for a pending row);
|
|
117
126
|
archive the surrounding story.
|
|
@@ -261,7 +261,12 @@ TUI 피커 없음 (settings-api `setSystemShell`만 존재).
|
|
|
261
261
|
### 스케줄 / 웹훅 ("스케줄 추가", "웹훅")
|
|
262
262
|
|
|
263
263
|
1. **확인**: `/schedules` / `/webhooks` (둘 다 `/channels` 허브의 섹션 딥링크).
|
|
264
|
-
2. **변경**: 해당 피커에서
|
|
264
|
+
2. **변경**: 해당 피커에서 추가/편집.
|
|
265
|
+
- **스케줄 저장소 = PG 테이블 `scheduler.schedules`** (더 이상 `<mixdogData>/schedules/<n>/SCHEDULE.md` 파일 아님). 관리 API `saveSchedule/deleteSchedule/setScheduleEnabled/listSchedules` (`channel-admin.mjs`) → 스토어 `schedules-db.mjs`.
|
|
266
|
+
- **필드**: 반복형은 `time`(5·6필드 cron) + 선택 `days`(cron 요일필드로 접힘: daily→`*`, weekday→`1-5`, weekend→`0,6`, `mon,wed,fri`/`1,3,5`→숫자; 못 매핑하면 에러) → `when_cron`. 1회성은 `at`(datetime) → `when_at`. `time`·`at`은 **택1**(스토어 XOR). `channel` 지정 시 `target=channel`+`channel_id`(이때 `model` 필수), 없으면 `target=session`. 본문(instructions)=`prompt`.
|
|
267
|
+
- **레거시 마이그레이션**: 기존 `schedules/` 디렉터리는 스토어 첫 init 시 SCHEDULE.md들을 테이블로 1회 자동 임포트(같은 days→cron 접힘) 후 디렉터리를 `schedules.migrated`로 rename(삭제 안 함). 한 줄 요약 로그.
|
|
268
|
+
- **웹훅 저장소 = PG 테이블 `webhooks.endpoints`** (더 이상 `<mixdogData>/webhooks/<n>/WEBHOOK.md` + `secret` 파일 아님). 관리 API `saveWebhook/deleteWebhook/setWebhookEnabled/listWebhooks` (`channel-admin.mjs`) → 스토어 `webhooks-db.mjs`(`upsertEndpoint/deleteEndpoint/setEndpointEnabled/listEndpoints`). 필드: `parser`(github·generic·stripe·sentry), 선택 `channel`(지정 시 `model` 필수)→`channel_id`, `secret`(미지정 시 랜덤 생성, 컬럼에 저장), 본문(instructions), `enabled`.
|
|
269
|
+
- **웹훅 레거시 마이그레이션**: 기존 `webhooks/` 디렉터리는 스토어 첫 init 시 각 `WEBHOOK.md`+`secret`을 `upsertEndpoint`로 1회 자동 임포트 후 디렉터리를 **삭제**(rename 아님 — 사용자 선택). 부분 실패 시 디렉터리 보존·로그·다음 부팅 재시도. 옛 per-endpoint deliveries(중복제거 이력)는 임포트 안 함(리셋 허용).
|
|
265
270
|
3. **검증**: 피커 목록 반영; 스케줄은 다음 발동 시각 표시.
|
|
266
271
|
|
|
267
272
|
### 원격 세션 ("리모트 가져와")
|
|
@@ -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.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Tool Use
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
- Every turn carries ALL independent calls you can already specify — a
|
|
4
|
+
second consecutive single-lookup turn is a defect, not a style choice;
|
|
5
|
+
serialize only on real data dependency. Merge variants/scopes into ONE
|
|
6
|
+
call wherever the schema accepts arrays (`pattern[]`, `path[]`,
|
|
7
|
+
`symbols[]`, `query[]`).
|
|
6
8
|
- Route by what is already known: known symbol/relation → `code_graph`;
|
|
7
9
|
exact text in a known scope → `grep`; unknown location, machine-wide/
|
|
8
10
|
out-of-repo whereabouts, or concept-level question → `explore` (which uses
|
|
@@ -31,4 +33,6 @@
|
|
|
31
33
|
unit (function/section) — over-read once instead of paging the same file
|
|
32
34
|
in small windows across turns; a 3rd window into one file means the first
|
|
33
35
|
should have been wider.
|
|
34
|
-
- Don't mix `apply_patch` with shell or other state-changing calls in one
|
|
36
|
+
- Don't mix `apply_patch` with shell or other state-changing calls in one
|
|
37
|
+
turn; batch independent-file patches in one turn, then verify them all in
|
|
38
|
+
ONE shell call the next.
|
|
@@ -3,6 +3,7 @@ import { appendFile } from 'fs/promises';
|
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { getPluginData } from './config.mjs';
|
|
6
|
+
import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../../shared/service-discovery.mjs';
|
|
6
7
|
|
|
7
8
|
const WARNED_KEYS = new Set();
|
|
8
9
|
|
|
@@ -15,6 +16,10 @@ const _FLUSH_INTERVAL_MS = 5000;
|
|
|
15
16
|
const _FLUSH_BATCH_SIZE = 500;
|
|
16
17
|
let _flushTimer = null;
|
|
17
18
|
let _serviceUrl = null;
|
|
19
|
+
// Port of the discovery advert `_serviceUrl` was resolved from (null when it
|
|
20
|
+
// came from legacy active-instance.json). Lets a flush connect-failure distrust
|
|
21
|
+
// a recycled-pid corpse advert so the next resolve falls back to legacy/buffer.
|
|
22
|
+
let _serviceAdvertPort = null;
|
|
18
23
|
let _flushInFlight = false;
|
|
19
24
|
let _localTracePath = null;
|
|
20
25
|
let _localTraceBuffer = [];
|
|
@@ -197,6 +202,11 @@ try {
|
|
|
197
202
|
function _resolveServiceUrl() {
|
|
198
203
|
if (_serviceUrl) return _serviceUrl;
|
|
199
204
|
try {
|
|
205
|
+
// Prefer the single-writer discovery advert (discovery/memory.json),
|
|
206
|
+
// pid-validated; fall back to legacy active-instance.json memory_port.
|
|
207
|
+
const advertPort = readServicePort('memory', { requirePid: false });
|
|
208
|
+
if (advertPort) { _serviceAdvertPort = advertPort; _serviceUrl = `http://127.0.0.1:${advertPort}`; return _serviceUrl; }
|
|
209
|
+
_serviceAdvertPort = null;
|
|
200
210
|
const runtimeRoot = process.env.MIXDOG_RUNTIME_ROOT
|
|
201
211
|
? join(process.env.MIXDOG_RUNTIME_ROOT)
|
|
202
212
|
: join(os.tmpdir(), 'mixdog');
|
|
@@ -245,6 +255,13 @@ async function _flush() {
|
|
|
245
255
|
}
|
|
246
256
|
} catch (err) {
|
|
247
257
|
_serviceUrl = null;
|
|
258
|
+
// Discovery-first consumer with no health probe: a connect failure
|
|
259
|
+
// means the pid-validated advert points at a dead (recycled-pid)
|
|
260
|
+
// port. Distrust it (connection-level errors ONLY — a timeout is a
|
|
261
|
+
// slow-but-alive daemon, not a corpse) so the next resolve falls back
|
|
262
|
+
// to legacy/buffer instead of re-trusting the same advert.
|
|
263
|
+
if (_serviceAdvertPort && isConnRefuseError(err)) markServiceUnreachable('memory', _serviceAdvertPort);
|
|
264
|
+
_serviceAdvertPort = null;
|
|
248
265
|
warnAgentOnce('agent-trace:flush-fetch', `[agent-trace] flush fetch failed (${err?.message}) — dropping batch`);
|
|
249
266
|
}
|
|
250
267
|
if (_buffer.length >= _FLUSH_BATCH_SIZE) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// tree (taskkill /T /F on Windows, SIGTERM->SIGKILL of every descendant on
|
|
10
10
|
// POSIX). No external dependencies.
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
|
+
import { detachedSpawnOpts } from '../../../shared/spawn-flags.mjs';
|
|
12
13
|
|
|
13
14
|
const isWin = process.platform === 'win32';
|
|
14
15
|
|
|
@@ -107,9 +108,8 @@ export function killStdioChildTreeFast(transport) {
|
|
|
107
108
|
if (isWin) {
|
|
108
109
|
try {
|
|
109
110
|
const cp = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
|
110
|
-
windowsHide: true,
|
|
111
|
-
detached: true,
|
|
112
111
|
stdio: 'ignore',
|
|
112
|
+
...detachedSpawnOpts,
|
|
113
113
|
});
|
|
114
114
|
cp.unref();
|
|
115
115
|
} catch { /* ignore */ }
|