mixdog 0.9.135 → 0.9.136
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 +3 -1
- package/scripts/tool-stress.mjs +8 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/defaults/skills/setup/SKILL.md +8 -43
- package/src/help.mjs +0 -2
- package/src/rules/lead/01-general.md +0 -4
- package/src/rules/shared/01-tool.md +10 -4
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +30 -4
- package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -5
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +17 -4
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +150 -41
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +34 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +18 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +10 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +124 -18
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +39 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +82 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +78 -12
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +34 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -5
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/channels/lib/config.mjs +14 -101
- package/src/runtime/channels/lib/index-drop-trace.mjs +2 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +4 -80
- package/src/runtime/channels/lib/scheduler.mjs +75 -18
- package/src/runtime/channels/lib/status-snapshot.mjs +5 -68
- package/src/runtime/channels/lib/tool-dispatch.mjs +12 -55
- package/src/runtime/channels/lib/worker-ipc.mjs +1 -16
- package/src/runtime/channels/lib/worker-main.mjs +22 -307
- package/src/runtime/media/renditions.mjs +68 -5
- package/src/runtime/media/renditions.test.mjs +45 -0
- package/src/runtime/media/store.mjs +1 -0
- package/src/runtime/memory/index.mjs +4 -3
- package/src/runtime/memory/lib/cycle-scheduler.mjs +17 -6
- package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +7 -1
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +13 -0
- package/src/runtime/shared/config.mjs +8 -49
- package/src/runtime/shared/schedule-time.mjs +25 -0
- package/src/runtime/shared/schedule-time.test.mjs +29 -0
- package/src/runtime/shared/schedules-db.mjs +73 -1
- package/src/runtime/shared/tool-card-model.mjs +1 -1
- package/src/runtime/shared/transcript-writer.mjs +10 -17
- package/src/runtime/shared/update-checker.mjs +7 -2
- package/src/runtime/shared/update-checker.test.mjs +33 -0
- package/src/session-runtime/channel-config-api.mjs +3 -20
- package/src/session-runtime/config-lifecycle.mjs +1 -42
- package/src/session-runtime/lifecycle-api.mjs +1 -19
- package/src/session-runtime/model-route-api.mjs +0 -2
- package/src/session-runtime/model-settings-persist.test.mjs +0 -1
- package/src/session-runtime/prewarm.mjs +2 -7
- package/src/session-runtime/prewarm.test.mjs +0 -1
- package/src/session-runtime/runtime-core.mjs +6 -185
- package/src/session-runtime/session-lifecycle.mjs +4 -17
- package/src/session-runtime/session-transcript.mjs +54 -0
- package/src/session-runtime/session-turn-api.mjs +4 -44
- package/src/session-runtime/settings-api.mjs +0 -45
- package/src/session-runtime/tool-catalog-schema.mjs +1 -1
- package/src/standalone/channel-admin.mjs +8 -150
- package/src/standalone/daemon.mjs +5 -48
- package/src/standalone/seeds.mjs +18 -1
- package/src/standalone/session-protocol.mjs +0 -8
- package/src/standalone/session-service.mjs +1 -3
- package/src/tui/App.jsx +0 -5
- package/src/tui/app/app-view.jsx +1 -2
- package/src/tui/app/create-app-pickers.mjs +0 -20
- package/src/tui/app/doctor.mjs +2 -6
- package/src/tui/app/prompt-submit.mjs +2 -49
- package/src/tui/app/settings-picker.mjs +22 -96
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +0 -17
- package/src/tui/app/use-prompt-handlers.mjs +4 -1
- package/src/tui/components/StatusLine.jsx +4 -4
- package/src/tui/dist/index.mjs +1003 -1461
- package/src/tui/session/context-state.mjs +0 -1
- package/src/tui/session/queue-helpers.mjs +1 -1
- package/src/tui/session/queue-helpers.test.mjs +10 -1
- package/src/tui/session/session-api-ext.mjs +0 -89
- package/src/tui/session/session-api.mjs +0 -2
- package/src/tui/session/tool-card-results.mjs +1 -1
- package/src/tui/session/tool-result-status.mjs +3 -3
- package/src/tui/session/tool-result-status.test.mjs +3 -3
- package/src/tui/session-local.mjs +0 -26
- package/src/workflows/default/WORKFLOW.md +13 -15
- package/src/workflows/solo/WORKFLOW.md +6 -7
- package/src/runtime/channels/lib/config-lock.mjs +0 -13
- package/src/runtime/channels/lib/drop-trace.mjs +0 -71
- package/src/runtime/channels/lib/format.mjs +0 -190
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -387
- package/src/runtime/channels/lib/inbound-routing.mjs +0 -81
- package/src/runtime/channels/lib/interaction-handlers.mjs +0 -70
- package/src/runtime/channels/lib/interaction-workflows.mjs +0 -76
- package/src/runtime/channels/lib/memory-client.mjs +0 -289
- package/src/runtime/channels/lib/output-forwarder.mjs +0 -934
- package/src/runtime/channels/lib/session-control.mjs +0 -36
- package/src/runtime/channels/lib/session-discovery.mjs +0 -217
- package/src/runtime/channels/lib/session-discovery.test.mjs +0 -32
- package/src/runtime/channels/lib/telegram-format.mjs +0 -280
- package/src/runtime/channels/lib/tool-format.mjs +0 -125
- package/src/runtime/channels/lib/transcript-binding.mjs +0 -336
- package/src/runtime/channels/lib/transcript-discovery.mjs +0 -208
- package/src/runtime/channels/providers/discord-access.mjs +0 -30
- package/src/runtime/channels/providers/discord-attachments.mjs +0 -69
- package/src/runtime/channels/providers/discord-gateway.mjs +0 -230
- package/src/runtime/channels/providers/discord.mjs +0 -730
- package/src/runtime/channels/providers/telegram.mjs +0 -598
- package/src/runtime/channels/providers/telegram.test.mjs +0 -75
- package/src/session-runtime/remote-control.mjs +0 -126
- package/src/session-runtime/remote-transcript.mjs +0 -134
- package/src/session-runtime/remote-transition-queue.mjs +0 -14
- package/src/tui/app/channel-pickers.mjs +0 -384
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.136",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -86,6 +86,8 @@
|
|
|
86
86
|
"bench:session-context": "node scripts/session-context-bench.mjs",
|
|
87
87
|
"bench:session-transport": "node scripts/session-transport-bench.mjs",
|
|
88
88
|
"bench:tool-overhead": "node scripts/tool-overhead-microbench.mjs",
|
|
89
|
+
"bench:search-tools": "node scripts/tool-search-bench.mjs",
|
|
90
|
+
"bench:io-tools": "node scripts/tool-io-bench.mjs",
|
|
89
91
|
"bench:internal-comms": "node scripts/internal-comms-bench.mjs",
|
|
90
92
|
"audit:models": "node scripts/model-catalog-audit.mjs",
|
|
91
93
|
"patch:replay": "node scripts/patch-replay.mjs",
|
package/scripts/tool-stress.mjs
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// concurrent multi-session waves and checks that results stay correct and
|
|
3
3
|
// bounded under load (no unhandled rejections, no resource-pressure failures,
|
|
4
4
|
// patch/read integrity preserved, cancellation clean). One-shot script:
|
|
5
|
-
// `node scripts/tool-stress.mjs`. Exit 0 = stable, 1 = instability found.
|
|
5
|
+
// `node scripts/tool-stress.mjs --unsafe-live`. Exit 0 = stable, 1 = instability found.
|
|
6
|
+
// This intentionally exercises shell cancellation and concurrent mutations.
|
|
7
|
+
// Use tool-search-bench.mjs for routine, read-only exploration diagnostics.
|
|
6
8
|
import '../src/runtime/shared/uv-threadpool-boot.mjs';
|
|
7
9
|
import './native-spawn-test-runtime.mjs';
|
|
8
10
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
@@ -17,6 +19,11 @@ import { warmNativeSpawnServer } from '../src/runtime/agent/orchestrator/tools/l
|
|
|
17
19
|
import { warmNativeSearchServer } from '../src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs';
|
|
18
20
|
import { prewarmFindEnumeration } from '../src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs';
|
|
19
21
|
|
|
22
|
+
if (!process.argv.includes('--unsafe-live')) {
|
|
23
|
+
console.error('Refusing high-impact tool stress without --unsafe-live; run `node scripts/tool-search-bench.mjs` for safe exploration diagnostics.');
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
|
21
28
|
const SESSIONS = 8;
|
|
22
29
|
const WAVES = 5;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "maintainer",
|
|
3
3
|
"name": "Maintainer",
|
|
4
|
-
"description": "Use for memory upkeep and
|
|
4
|
+
"description": "Use for internal Git operations, memory upkeep, session naming, and other Mixdog background operations.",
|
|
5
5
|
"entry": "AGENT.md"
|
|
6
6
|
}
|
|
@@ -18,7 +18,7 @@ description: Use this skill only to inspect or modify a Mixdog user's persisted
|
|
|
18
18
|
- 프로젝트 config: `<cwd>/.mcp.json`, `<cwd>/.mixdog/skills/`, `<cwd>/.mixdog/hooks.json`
|
|
19
19
|
- TUI 명령 목록: `src/tui/app/slash-commands.mjs`
|
|
20
20
|
- TUI 설정 허브: `/setting` (별칭 `/settings`, `/config`) → `src/tui/app/settings-picker.mjs`
|
|
21
|
-
- Desktop 설정: `Ctrl+,` → General / Context / Output style / Providers / Git / Skills / MCP / Plugins / Hooks /
|
|
21
|
+
- Desktop 설정: `Ctrl+,` → General / Context / Output style / Providers / Git / Skills / MCP / Plugins / Hooks / Connection / System / Shortcuts / About
|
|
22
22
|
- Desktop rail: **Projects / Workflows / Schedules / Webhooks / Utilities**
|
|
23
23
|
|
|
24
24
|
사용자 경로와 지원 여부는 **현재 Desktop 구현을 최신 기준**으로 판정한다. TUI는 Desktop에 없는 고급 경로를 보완할 때만 사용하며, 저장만 왕복되고 실제 소비되지 않는 key는 사용자 옵션으로 설명하지 않는다.
|
|
@@ -34,10 +34,9 @@ description: Use this skill only to inspect or modify a Mixdog user's persisted
|
|
|
34
34
|
| Search 모델 | `agent.searchRoute` |
|
|
35
35
|
| Provider·MCP·프로필·스킬·업데이트·셸·autoClear·compaction | `agent.*` |
|
|
36
36
|
| Web search 도구 | `agent.modules.search.enabled` |
|
|
37
|
-
| Channels messaging | `agent.modules.channels.enabled` |
|
|
38
37
|
| Recap(배경 사이클) | `agent.recap.enabled` |
|
|
39
38
|
| 출력 스타일 | 루트 `outputStyle` |
|
|
40
|
-
|
|
|
39
|
+
| Webhook server | `channels.webhook.*` |
|
|
41
40
|
| 메모리 cycle interval | `memory.cycle1.interval`, `memory.cycle2.interval` |
|
|
42
41
|
| 음성 전사 | `voice.enabled` |
|
|
43
42
|
| TUI 테마 | `ui.theme` |
|
|
@@ -51,7 +50,7 @@ API 키·토큰·OAuth 자격은 config에 쓰지 않는다.
|
|
|
51
50
|
- MCP: `/mcp`, Desktop **Settings → MCP**, 또는 `mcpStatus()`
|
|
52
51
|
- Skills: `/skills`, Desktop **Settings → Skills**, 또는 `skillsStatus()`
|
|
53
52
|
- Providers: `/providers`, Desktop **Settings → Providers**, 또는 `getProviderSetup()`
|
|
54
|
-
-
|
|
53
|
+
- Webhooks: Desktop **Webhooks** 뷰, 또는 `getChannelSetup()` (webhook enabled/port)
|
|
55
54
|
- 비밀 존재 여부: `hasStoredSecret(account)`만 사용하고 값은 출력하지 않는다.
|
|
56
55
|
|
|
57
56
|
---
|
|
@@ -175,8 +174,6 @@ API 키·토큰·OAuth 자격은 config에 쓰지 않는다.
|
|
|
175
174
|
| `agent.<provider>.apiKey` | Provider API key |
|
|
176
175
|
| `agent.openai.usageSessionKey` | OpenAI usage |
|
|
177
176
|
| `agent.opencode-go.authCookie` | OpenCode Go usage |
|
|
178
|
-
| `discord.token` | Discord bot |
|
|
179
|
-
| `telegram.token` | Telegram bot |
|
|
180
177
|
|
|
181
178
|
값은 출력하지 않고 `hasStoredSecret()`으로 존재만 확인한다. MCP `env`는 keychain이 아니라 자식 프로세스 환경이다.
|
|
182
179
|
|
|
@@ -335,21 +332,9 @@ Plugins 화면에는 일반적인 “plugin 활성/비활성” toggle이 없다
|
|
|
335
332
|
|
|
336
333
|
---
|
|
337
334
|
|
|
338
|
-
## 5.
|
|
335
|
+
## 5. Voice·Automation
|
|
339
336
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
1. **확인**: `/channels`, `/setting → Channel / Setting`, Desktop **Settings → Channels**, `getChannelSetup()`.
|
|
343
|
-
2. **변경**:
|
|
344
|
-
- provider: Discord/Telegram 선택 → `setChannelProvider()`
|
|
345
|
-
- token: Bot token → OS keychain
|
|
346
|
-
- main target: Discord channel ID / Telegram chat ID → `setChannel({ provider, channelId })`
|
|
347
|
-
3. **저장**: provider·target은 `channels.provider`, `channels.channel.discordChannelId` / `telegramChatId`. token은 `discord.token` / `telegram.token`.
|
|
348
|
-
4. **검증**: active provider, authenticated, main target의 Ready 상태.
|
|
349
|
-
|
|
350
|
-
`Channels enabled`는 messaging만 제어하며 디스크 경로는 `agent.modules.channels.enabled`다. schedules/webhooks는 이 toggle과 독립적으로 계속 실행된다.
|
|
351
|
-
|
|
352
|
-
`channels.backend`와 `channels.channel.channelId`는 쓰지 않는다. canonicalize가 제거하고 `provider` / per-provider id만 남긴다.
|
|
337
|
+
Discord/Telegram messaging은 제거됐다. schedules/webhooks 자동화와 voice transcription만 남는다.
|
|
353
338
|
|
|
354
339
|
### Voice transcription
|
|
355
340
|
|
|
@@ -383,16 +368,7 @@ Plugins 화면에는 일반적인 “plugin 활성/비활성” toggle이 없다
|
|
|
383
368
|
|
|
384
369
|
기존 파일형 schedules는 최초 DB init 때 import 후 `schedules.migrated`로 보존한다. 기존 webhook 디렉터리는 성공적으로 모두 import된 경우 삭제한다.
|
|
385
370
|
|
|
386
|
-
활성 schedule/webhook이 있으면 automation worker가
|
|
387
|
-
|
|
388
|
-
### Remote session
|
|
389
|
-
|
|
390
|
-
1. **강제 claim**: TUI `/remote`는 항상 현재 세션을 ON으로 만들고 기존 owner를 넘겨받는다.
|
|
391
|
-
2. **TUI toggle**: `/channels → Remote Runtime` 또는 `/setting → Remote Runtime`.
|
|
392
|
-
3. **Desktop**: Settings에 Remote 행이 없다. session header의 remote toggle만 사용한다. draft의 New-task remote는 일회성이며 설정 기본값이 아니다.
|
|
393
|
-
4. **시작 시 ON**: CLI `mixdog --remote`.
|
|
394
|
-
5. **중요**: config `remote.autoStart`는 사용하지 않는다. 자동화 worker의 자동 시작과 remote session claim은 별개다.
|
|
395
|
-
6. **검증**: `isRemoteEnabled()`, owner session ID, channel worker status.
|
|
371
|
+
활성 schedule/webhook이 있으면 automation worker가 자동 시작한다.
|
|
396
372
|
|
|
397
373
|
---
|
|
398
374
|
|
|
@@ -436,13 +412,12 @@ Desktop과 현재 runtime에서 소비되지 않는 아래 key는 사용자 옵
|
|
|
436
412
|
| `agent.agentMaintenance`, `agent.runtime` | 제거 |
|
|
437
413
|
| `remote.autoStart` | 제거; Remote는 session header에서 수동 claim |
|
|
438
414
|
| `ui.mouseMode` | 제거; `ui.theme`은 TUI 현행 설정 |
|
|
439
|
-
| `channels.backend` |
|
|
440
|
-
| `channels.channel.channelId` | 제거; `discordChannelId` / `telegramChatId`만 유지 |
|
|
415
|
+
| `channels.backend`, `channels.provider`, `channels.channel.*` | 제거; Discord/Telegram messaging 은퇴로 전부 데드 key |
|
|
441
416
|
| `channels.quiet`, `channels.schedules` | 제거; schedule은 PG가 단일 저장소 |
|
|
442
417
|
| `channels.webhook.ngrokDomain`, `channels.webhook.respectQuiet` | 제거; endpoint URL은 Desktop Webhooks가 발급 |
|
|
443
418
|
| `agent.outputStyle` | 제거 (이관 없음; 루트 `outputStyle`만 유효) |
|
|
444
419
|
|
|
445
|
-
다음 항목은 레거시처럼 보여도 현행이므로 유지한다: 루트 `outputStyle`, `agent.shell`, `agent.modules`,
|
|
420
|
+
다음 항목은 레거시처럼 보여도 현행이므로 유지한다: 루트 `outputStyle`, `agent.shell`, `agent.modules`, compaction의 `type/compactType`과 recall tuning fields.
|
|
446
421
|
|
|
447
422
|
---
|
|
448
423
|
|
|
@@ -478,21 +453,11 @@ URL transport는 `type` + `url` + 선택 `headers`를 사용한다.
|
|
|
478
453
|
```json
|
|
479
454
|
{
|
|
480
455
|
"channels": {
|
|
481
|
-
"provider": "discord",
|
|
482
|
-
"channel": {
|
|
483
|
-
"discordChannelId": "",
|
|
484
|
-
"telegramChatId": ""
|
|
485
|
-
},
|
|
486
456
|
"webhook": {
|
|
487
457
|
"enabled": true,
|
|
488
458
|
"port": 3333
|
|
489
459
|
}
|
|
490
460
|
},
|
|
491
|
-
"agent": {
|
|
492
|
-
"modules": {
|
|
493
|
-
"channels": { "enabled": true }
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
461
|
"voice": { "enabled": false }
|
|
497
462
|
}
|
|
498
463
|
```
|
package/src/help.mjs
CHANGED
|
@@ -46,8 +46,6 @@ export const HELP_LINES = [
|
|
|
46
46
|
' /plugins manage local plugin integrations',
|
|
47
47
|
' /hooks manage before-tool hook rules and events',
|
|
48
48
|
' /providers manage auth, API keys, OAuth, and local endpoints',
|
|
49
|
-
' /channels manage Discord, Telegram, and voice',
|
|
50
|
-
' /remote claim remote for this session',
|
|
51
49
|
' /setting open runtime settings (aliases: /settings, /config)',
|
|
52
50
|
' /profile set your title and response language',
|
|
53
51
|
' /update check version and update mixdog',
|
|
@@ -7,10 +7,6 @@
|
|
|
7
7
|
- Confirm destructive/hard-to-reverse actions against explicit validated paths;
|
|
8
8
|
never `~`, a root, or unresolved variables/globs; report material deletion
|
|
9
9
|
recoverability.
|
|
10
|
-
- After required work, run final verification only when the outcome needs
|
|
11
|
-
evidence the successful tool result does not already give, and run only
|
|
12
|
-
affected blocking invariants. Combine commands when dependency or
|
|
13
|
-
atomicity requires it.
|
|
14
10
|
- Mid-task: replacement supersedes; addition folds in; status gets a brief
|
|
15
11
|
answer while work continues. After compaction, resume the summary.
|
|
16
12
|
- Final text ends the turn only when done.
|
|
@@ -41,7 +41,12 @@
|
|
|
41
41
|
a file to rebuild context or confirm an edit, no rerun of a passed check.
|
|
42
42
|
A hole (needed content absent and not reconstructable) is fetched once;
|
|
43
43
|
a change re-opens only that hole. Returned output is fully mined before the
|
|
44
|
-
next round.
|
|
44
|
+
next round. A successful edit establishes the resulting source as known
|
|
45
|
+
state: do not inspect or query changed source with `read`, `grep`,
|
|
46
|
+
`code_graph`, `glob`, or `list`; proceed directly to final verification.
|
|
47
|
+
Reopen only a specific hole reported by a failed or partial edit, or by a
|
|
48
|
+
failed verification.
|
|
49
|
+
`code_graph references` supplies the declaration and scoped
|
|
45
50
|
usages and ends that facet; values/locations end at the context grep
|
|
46
51
|
returns; `read` covers only what returned spans cannot, as an anchored
|
|
47
52
|
offset/limit window. Already obtained hunk text is any visible span —
|
|
@@ -73,9 +78,10 @@
|
|
|
73
78
|
and buildability invariants. Treat mutable behavior, UX, exact text,
|
|
74
79
|
snapshots, and implementation shape as advisory specifications; update them
|
|
75
80
|
when the requested behavior changes instead of preserving obsolete behavior.
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
- When the applied changes require verification, run all applicable checks as
|
|
82
|
+
one final batch only after every planned edit is complete — never after
|
|
83
|
+
individual edits. If that verification fails, fully collect the failures,
|
|
84
|
+
batch all determinable fixes, then rerun only the affected failed checks once.
|
|
79
85
|
- If inspection can change evidence or durable state, use read-only means;
|
|
80
86
|
mutate only when the deliverable requires it, first preserving evidence
|
|
81
87
|
at risk. Never mutate merely to clear an obstacle or unexpected state;
|
|
@@ -423,11 +423,36 @@ function traceAgentToolFailure({ sessionId, iteration, toolName, toolKind, toolM
|
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
function
|
|
426
|
+
function summarizeToolTiming(toolTiming) {
|
|
427
|
+
if (!toolTiming || typeof toolTiming !== 'object') return null;
|
|
428
|
+
const number = (key) => {
|
|
429
|
+
const value = Number(toolTiming[key]);
|
|
430
|
+
return Number.isFinite(value) ? value : null;
|
|
431
|
+
};
|
|
432
|
+
const dispatchStartedAt = number('dispatchStartedAt');
|
|
433
|
+
const executionStartedAt = number('executionStartedAt');
|
|
434
|
+
const executionCompletedAt = number('executionCompletedAt');
|
|
435
|
+
const postprocessStartedAt = number('postprocessStartedAt');
|
|
436
|
+
const resultCompletedAt = number('resultCompletedAt');
|
|
437
|
+
const timing = {};
|
|
438
|
+
const duration = (key, start, end) => {
|
|
439
|
+
if (start === null || end === null) return;
|
|
440
|
+
timing[key] = Math.max(0, Math.round(end - start));
|
|
441
|
+
};
|
|
442
|
+
duration('dispatch_wait_ms', dispatchStartedAt, executionStartedAt);
|
|
443
|
+
duration('execution_ms', executionStartedAt, executionCompletedAt);
|
|
444
|
+
duration('result_collection_wait_ms', executionCompletedAt, postprocessStartedAt);
|
|
445
|
+
duration('postprocess_ms', postprocessStartedAt, resultCompletedAt);
|
|
446
|
+
duration('total_ms', dispatchStartedAt, resultCompletedAt);
|
|
447
|
+
return Object.keys(timing).length > 0 ? timing : null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function traceAgentTool({ sessionId, iteration, toolName, toolKind, toolMs, toolArgs, agent, resultKind, model, resultText, localSearchTelemetry = null, resultTelemetry = null, toolTiming = null, cwd }) {
|
|
427
451
|
const nextCallCount = countJsonNextCalls(resultText);
|
|
428
452
|
const resultBytesEst = typeof resultText === 'string' ? Buffer.byteLength(resultText, 'utf8') : 0;
|
|
429
453
|
const resultLinesEst = typeof resultText === 'string' && resultText.length > 0 ? resultText.split('\n').length : 0;
|
|
430
454
|
const numericToolMs = Number(toolMs);
|
|
455
|
+
const summarizedTiming = summarizeToolTiming(toolTiming);
|
|
431
456
|
const summarizedArgs = summarizeToolArgs(toolName, toolArgs);
|
|
432
457
|
const grepCoverage = parseGrepCoverage(resultText, toolName, toolArgs, resultKind);
|
|
433
458
|
// Hash the FULL args, not the summary: summaries drop payload fields
|
|
@@ -474,9 +499,10 @@ function traceAgentTool({ sessionId, iteration, toolName, toolKind, toolMs, tool
|
|
|
474
499
|
local_search: localSearchTelemetry && Object.keys(localSearchTelemetry).length > 0
|
|
475
500
|
? { ...localSearchTelemetry }
|
|
476
501
|
: null,
|
|
477
|
-
payload:
|
|
478
|
-
? {
|
|
479
|
-
: {},
|
|
502
|
+
payload: {
|
|
503
|
+
...(summarizedTiming ? { timing: summarizedTiming } : {}),
|
|
504
|
+
...(resultTelemetry?.integrity ? { integrity: { ...resultTelemetry.integrity } } : {}),
|
|
505
|
+
},
|
|
480
506
|
cwd: cwd || null,
|
|
481
507
|
});
|
|
482
508
|
if (
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
test('tool traces expose dispatch, execution, collection, and postprocess timing', () => {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), 'mixdog-tool-timing-'));
|
|
10
|
+
try {
|
|
11
|
+
const tracePath = join(dir, 'agent-trace.jsonl');
|
|
12
|
+
const child = spawnSync(process.execPath, ['--input-type=module', '-e', `
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { traceAgentTool } from './src/runtime/agent/orchestrator/agent-trace-format.mjs';
|
|
15
|
+
import { drainAgentTrace } from './src/runtime/agent/orchestrator/agent-trace-io.mjs';
|
|
16
|
+
traceAgentTool({
|
|
17
|
+
sessionId: 'timing-test',
|
|
18
|
+
iteration: 1,
|
|
19
|
+
toolName: 'grep',
|
|
20
|
+
toolKind: 'builtin',
|
|
21
|
+
toolMs: 50,
|
|
22
|
+
resultKind: 'normal',
|
|
23
|
+
resultText: 'ok',
|
|
24
|
+
toolTiming: {
|
|
25
|
+
dispatchStartedAt: 100,
|
|
26
|
+
executionStartedAt: 110,
|
|
27
|
+
executionCompletedAt: 160,
|
|
28
|
+
postprocessStartedAt: 175,
|
|
29
|
+
resultCompletedAt: 190,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
await drainAgentTrace();
|
|
33
|
+
const row = JSON.parse(readFileSync(process.env.MIXDOG_AGENT_TRACE_PATH, 'utf8').trim());
|
|
34
|
+
process.stdout.write(JSON.stringify(row.payload.timing));
|
|
35
|
+
`], {
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
env: {
|
|
39
|
+
...process.env,
|
|
40
|
+
MIXDOG_AGENT_TRACE_PATH: tracePath,
|
|
41
|
+
MIXDOG_AGENT_TRACE_DISABLE: '',
|
|
42
|
+
MIXDOG_AGENT_TRACE_LOCAL_DISABLE: '',
|
|
43
|
+
MIXDOG_RUNTIME_ROOT: join(dir, 'no-service'),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
assert.equal(child.status, 0, child.stderr);
|
|
47
|
+
assert.deepEqual(JSON.parse(child.stdout), {
|
|
48
|
+
dispatch_wait_ms: 10,
|
|
49
|
+
execution_ms: 50,
|
|
50
|
+
result_collection_wait_ms: 15,
|
|
51
|
+
postprocess_ms: 15,
|
|
52
|
+
total_ms: 90,
|
|
53
|
+
});
|
|
54
|
+
} finally {
|
|
55
|
+
rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
@@ -50,7 +50,7 @@ function _maybeSweepDiskCache() {
|
|
|
50
50
|
void _sweepDiskCache();
|
|
51
51
|
}
|
|
52
52
|
function _diskPath(absPath) {
|
|
53
|
-
const hash = createHash('sha256').update(absPath).digest('hex')
|
|
53
|
+
const hash = createHash('sha256').update(absPath).digest('hex');
|
|
54
54
|
return join(DISK_CACHE_DIR, `${hash}.json`);
|
|
55
55
|
}
|
|
56
56
|
function _readDiskEntry(absPath) {
|
|
@@ -58,7 +58,7 @@ function _readDiskEntry(absPath) {
|
|
|
58
58
|
if (!existsSync(p)) return null;
|
|
59
59
|
try {
|
|
60
60
|
const parsed = JSON.parse(readFileSync(p, 'utf8'));
|
|
61
|
-
if (!parsed || typeof parsed.content !== 'string' || !parsed.stat) return null;
|
|
61
|
+
if (!parsed || parsed.absPath !== absPath || typeof parsed.content !== 'string' || !parsed.stat) return null;
|
|
62
62
|
return { content: parsed.content, stat: parsed.stat, ts: parsed.ts };
|
|
63
63
|
} catch { return null; }
|
|
64
64
|
}
|
|
@@ -157,7 +157,7 @@ export function invalidatePrefetchCache(path, cwd) {
|
|
|
157
157
|
_deleteDiskEntry(abs);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
function drainPrefetchDiskWrites() {
|
|
160
|
+
export function drainPrefetchDiskWrites() {
|
|
161
161
|
if (_diskWriteTimer !== null) {
|
|
162
162
|
clearTimeout(_diskWriteTimer);
|
|
163
163
|
_diskWriteTimer = null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
|
|
14
|
+
test('prefetch disk cache uses a full hash and rejects a mismatched path identity', async () => {
|
|
15
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-prefetch-cache-'));
|
|
16
|
+
const previousDataDir = process.env.MIXDOG_DATA_DIR;
|
|
17
|
+
process.env.MIXDOG_DATA_DIR = root;
|
|
18
|
+
const source = join(root, 'source.txt');
|
|
19
|
+
writeFileSync(source, 'source');
|
|
20
|
+
try {
|
|
21
|
+
const first = await import(`./prefetch-cache.mjs?writer=${Date.now()}`);
|
|
22
|
+
first.setPrefetchCached(source, 'cached output');
|
|
23
|
+
first.drainPrefetchDiskWrites();
|
|
24
|
+
const cacheDir = join(root, 'cache', 'prefetch');
|
|
25
|
+
const [cacheFile] = readdirSync(cacheDir);
|
|
26
|
+
assert.match(cacheFile, /^[0-9a-f]{64}\.json$/);
|
|
27
|
+
|
|
28
|
+
const path = join(cacheDir, cacheFile);
|
|
29
|
+
const payload = JSON.parse(readFileSync(path, 'utf8'));
|
|
30
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
31
|
+
writeFileSync(path, JSON.stringify({ ...payload, absPath: `${source}.other` }));
|
|
32
|
+
|
|
33
|
+
const second = await import(`./prefetch-cache.mjs?reader=${Date.now()}`);
|
|
34
|
+
assert.equal(second.tryPrefetchCached(source), null);
|
|
35
|
+
} finally {
|
|
36
|
+
if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
|
|
37
|
+
else process.env.MIXDOG_DATA_DIR = previousDataDir;
|
|
38
|
+
rmSync(root, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -726,6 +726,13 @@ export async function processToolBatch(ctx) {
|
|
|
726
726
|
resultText: result,
|
|
727
727
|
localSearchTelemetry: completed.localSearchTelemetry,
|
|
728
728
|
resultTelemetry: completed.resultTelemetry,
|
|
729
|
+
toolTiming: {
|
|
730
|
+
dispatchStartedAt,
|
|
731
|
+
executionStartedAt: executionStartedAt ?? toolStartedAt,
|
|
732
|
+
executionCompletedAt: toolEndedAt,
|
|
733
|
+
postprocessStartedAt,
|
|
734
|
+
resultCompletedAt: Date.now(),
|
|
735
|
+
},
|
|
729
736
|
cwd,
|
|
730
737
|
});
|
|
731
738
|
// Deferred writes that predate a later mutation are skipped;
|
|
@@ -89,11 +89,6 @@ export const BUILTIN_TOOLS = [
|
|
|
89
89
|
type: 'object',
|
|
90
90
|
properties: {
|
|
91
91
|
command: { type: 'string', description: `Command.${_shellSyntaxCheat}` },
|
|
92
|
-
timeout_ms: {
|
|
93
|
-
type: 'integer',
|
|
94
|
-
minimum: 0,
|
|
95
|
-
description: 'Hard total deadline in milliseconds; omit or use 0 to allow unlimited runtime after task promotion.',
|
|
96
|
-
},
|
|
97
92
|
...(_shellBackgroundDisabled ? {} : {
|
|
98
93
|
run_in_background: {
|
|
99
94
|
type: 'boolean',
|
|
@@ -359,9 +359,8 @@ export function getCachedReadOnlyStat(fullPath, loader = statSync, now = Date.no
|
|
|
359
359
|
return stat;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
async function visitPathStats(paths, workDir, concurrency, opts, visitor) {
|
|
363
363
|
const items = Array.isArray(paths) ? paths : [];
|
|
364
|
-
const out = new Array(items.length);
|
|
365
364
|
const now = Date.now();
|
|
366
365
|
const inflight = new Map();
|
|
367
366
|
let next = 0;
|
|
@@ -406,17 +405,31 @@ export async function statPathsForMtime(paths, workDir, concurrency = Infinity,
|
|
|
406
405
|
if (index >= items.length) return;
|
|
407
406
|
const p = items[index];
|
|
408
407
|
const full = isAbsolute(p) ? p : resolveAgainstCwd(p, workDir);
|
|
408
|
+
let entry;
|
|
409
409
|
try {
|
|
410
410
|
const stat = await resolveStat(full);
|
|
411
411
|
if (!stat) throw new Error('stat failed');
|
|
412
|
-
|
|
412
|
+
entry = { path: p, full, stat, size: stat.size, mtime: stat.mtimeMs, mtimeMs: stat.mtimeMs };
|
|
413
413
|
} catch {
|
|
414
|
-
|
|
414
|
+
entry = { path: p, full, stat: null, size: 0, mtime: 0, mtimeMs: 0 };
|
|
415
415
|
}
|
|
416
|
+
visitor(entry, index);
|
|
416
417
|
}
|
|
417
418
|
}
|
|
418
419
|
const workerCount = Math.min(Math.max(1, concurrency), Math.max(1, items.length));
|
|
419
420
|
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function visitPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}, visitor = () => {}) {
|
|
424
|
+
await visitPathStats(paths, workDir, concurrency, opts, visitor);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function statPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}) {
|
|
428
|
+
const items = Array.isArray(paths) ? paths : [];
|
|
429
|
+
const out = new Array(items.length);
|
|
430
|
+
await visitPathStats(items, workDir, concurrency, opts, (entry, index) => {
|
|
431
|
+
out[index] = entry;
|
|
432
|
+
});
|
|
420
433
|
return out;
|
|
421
434
|
}
|
|
422
435
|
|
|
@@ -8,7 +8,9 @@ import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node
|
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { tryExecuteExternalToolAdapter } from './external-tool-adapters.mjs';
|
|
11
|
+
import { executeBuiltinTool } from '../builtin.mjs';
|
|
11
12
|
import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
13
|
+
import { executePatchTool } from '../patch.mjs';
|
|
12
14
|
import { closeNativePatchServerForTests } from '../patch/native-server.mjs';
|
|
13
15
|
|
|
14
16
|
const ANCHOR = '#if 0\n"""\n#endif';
|
|
@@ -57,6 +59,44 @@ test('count drift falls through to the strict ambiguity reject', async (t) => {
|
|
|
57
59
|
assert.match(String(result), /current file excerpt lines/);
|
|
58
60
|
});
|
|
59
61
|
|
|
62
|
+
test('successful edit snapshot makes a follow-up read return unchanged', async (t) => {
|
|
63
|
+
const { dir, file } = makeTempFile('alpha\nkeep\n');
|
|
64
|
+
const sessionId = `edit-known-current-${process.pid}`;
|
|
65
|
+
t.after(() => { rmSync(dir, { recursive: true, force: true }); void closeNativePatchServerForTests?.(); });
|
|
66
|
+
|
|
67
|
+
const result = await tryExecuteExternalToolAdapter('edit', {
|
|
68
|
+
file_path: file,
|
|
69
|
+
old_string: 'alpha',
|
|
70
|
+
new_string: 'omega',
|
|
71
|
+
}, dir, { sessionId });
|
|
72
|
+
assert.match(String(result), /^Updated /);
|
|
73
|
+
|
|
74
|
+
const reread = await executeBuiltinTool('read', { path: file }, dir, { sessionId });
|
|
75
|
+
assert.equal(reread, `[file unchanged: ${file.replaceAll('\\', '/')}]`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('successful apply_patch snapshot makes a follow-up read return unchanged', async (t) => {
|
|
79
|
+
const { dir, file } = makeTempFile('alpha\nkeep\n');
|
|
80
|
+
const sessionId = `patch-known-current-${process.pid}`;
|
|
81
|
+
t.after(() => { rmSync(dir, { recursive: true, force: true }); void closeNativePatchServerForTests?.(); });
|
|
82
|
+
|
|
83
|
+
const result = await executePatchTool('apply_patch', {
|
|
84
|
+
base_path: dir,
|
|
85
|
+
patch: `*** Begin Patch
|
|
86
|
+
*** Update File: poly.c
|
|
87
|
+
@@
|
|
88
|
+
-alpha
|
|
89
|
+
+omega
|
|
90
|
+
keep
|
|
91
|
+
*** End Patch
|
|
92
|
+
`,
|
|
93
|
+
}, dir, { sessionId });
|
|
94
|
+
assert.match(String(result), /^Applied 1 File \(Native\)/);
|
|
95
|
+
|
|
96
|
+
const reread = await executeBuiltinTool('read', { path: file }, dir, { sessionId });
|
|
97
|
+
assert.equal(reread, `[file unchanged: ${file.replaceAll('\\', '/')}]`);
|
|
98
|
+
});
|
|
99
|
+
|
|
60
100
|
test('stale read snapshot does not block a still-unique current old_string', async (t) => {
|
|
61
101
|
const { dir, file } = makeTempFile('alpha\nkeep\n');
|
|
62
102
|
const sessionId = `edit-stale-safe-${process.pid}`;
|