mixdog 0.9.145 → 0.9.147
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 -2
- package/src/lib/rules-builder.cjs +12 -3
- package/src/output-styles/common.md +7 -9
- package/src/output-styles/simple.md +1 -1
- package/src/rules/shared/10-tool-workflow.md +18 -28
- package/src/rules/shared/40-editing.md +18 -6
- package/src/rules/shared/60-verification.md +0 -1
- package/src/rules/shared/70-delivery.md +4 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +132 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +82 -40
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +64 -29
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +24 -6
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +41 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +9 -49
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +78 -30
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +79 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +22 -10
- package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +30 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -1
- package/src/runtime/shared/edit-tool-dialect.mjs +8 -0
- package/src/runtime/shared/pristine-execution-contract.json +3 -0
- package/src/runtime/shared/task-notification-envelope.mjs +14 -0
- package/src/session-runtime/lifecycle-api.mjs +44 -1
- package/src/session-runtime/runtime-core.mjs +1 -0
- package/src/session-runtime/session-lifecycle.test.mjs +23 -0
- package/src/session-runtime/tool-policy-refresh.mjs +4 -1
- package/src/session-runtime/tool-policy-surface.test.mjs +23 -8
- package/src/tui/session/context-state.mjs +17 -3
- package/src/tui/session/session-api-ext.mjs +6 -1
- package/src/tui/session/session-api.mjs +6 -1
- package/src/tui/session/tool-card-results.mjs +7 -0
- package/src/tui/session/tool-result-status.mjs +31 -1
- package/src/tui/session/tool-result-status.test.mjs +30 -0
- package/src/tui/session/turn.mjs +12 -3
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
|
|
26
26
|
// Human-readable transport label for handshake/acquire error messages. Shared
|
|
27
27
|
// with openai-oauth-ws.mjs (stream-side errors use the same labels).
|
|
28
|
-
import { _envOn,
|
|
28
|
+
import { _envOn, _codexDashedId, _codexDashedIdV7, _codexBetaFeatures, _dumpHandshakeHeaders, _dumpFrame, _formatRedactedHeaders, _cfCookieHeader, _cfCookieCapture } from './openai-ws-headers.mjs';
|
|
29
29
|
export function _wsErrLabel(p) {
|
|
30
30
|
if (p === 'xai') return 'xAI WS';
|
|
31
31
|
if (p === 'openai-direct' || p === 'openai') return 'OpenAI WS';
|
|
@@ -76,6 +76,46 @@ const MAX_POOLED_SOCKETS_PER_KEY = 8;
|
|
|
76
76
|
export const _wsPool = new Map();
|
|
77
77
|
let _releaseSequence = 0;
|
|
78
78
|
|
|
79
|
+
// The server issues an `x-codex-turn-state` sticky-routing token on the
|
|
80
|
+
// handshake 101 response; it pins the backend node that holds the warm prefix.
|
|
81
|
+
// codex keeps that token in a turn-scoped cell and replays it on the RECONNECT
|
|
82
|
+
// handshake (build_responses_headers -> connect_websocket), so a replacement
|
|
83
|
+
// connection re-pins the same node instead of cold-starting the prefix. Our
|
|
84
|
+
// pool entry dies with its socket, so the token is retained here at poolKey
|
|
85
|
+
// scope and replayed only for a replacement socket. A parallel peer must still
|
|
86
|
+
// open clean: inheriting a sibling's token makes openai-oauth treat the request
|
|
87
|
+
// as a continuation of another in-flight turn ("No tool output found for
|
|
88
|
+
// function call ...").
|
|
89
|
+
const _turnStateByPoolKey = new Map();
|
|
90
|
+
const MAX_RETAINED_TURN_STATE_KEYS = 4096;
|
|
91
|
+
|
|
92
|
+
function _retainTurnState(poolKey, entry) {
|
|
93
|
+
if (!poolKey || !entry) return;
|
|
94
|
+
const turnState = typeof entry.turnState === 'string' ? entry.turnState : '';
|
|
95
|
+
// A token already retired by the per-turn drop guard must not be revived.
|
|
96
|
+
if (!turnState) return;
|
|
97
|
+
// Bounded like the other pool maps so a long-lived process cannot
|
|
98
|
+
// accumulate one token per dead session.
|
|
99
|
+
if (!_turnStateByPoolKey.has(poolKey) && _turnStateByPoolKey.size >= MAX_RETAINED_TURN_STATE_KEYS) {
|
|
100
|
+
const oldest = _turnStateByPoolKey.keys().next().value;
|
|
101
|
+
if (oldest !== undefined) _turnStateByPoolKey.delete(oldest);
|
|
102
|
+
}
|
|
103
|
+
_turnStateByPoolKey.set(poolKey, {
|
|
104
|
+
turnState,
|
|
105
|
+
// Carry the turn owner so the per-turn drop guard still retires a
|
|
106
|
+
// replayed token when turn_id moves on.
|
|
107
|
+
turnStateTurnId: entry.turnStateTurnId ?? null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _replacementTurnState(poolKey) {
|
|
112
|
+
if (!poolKey) return null;
|
|
113
|
+
// A live entry in the bucket means this handshake is a parallel peer, not
|
|
114
|
+
// a reconnect.
|
|
115
|
+
if (hasPooledWebSocket(poolKey)) return null;
|
|
116
|
+
return _turnStateByPoolKey.get(poolKey) || null;
|
|
117
|
+
}
|
|
118
|
+
|
|
79
119
|
function _poolCompatibility(auth, cacheKey) {
|
|
80
120
|
const type = String(auth?.type || 'openai-oauth');
|
|
81
121
|
// Prefer the stable OAuth account id so an access-token refresh can keep
|
|
@@ -139,6 +179,9 @@ function _removeFromPool(poolKey, entry) {
|
|
|
139
179
|
// idle-close or liveness-ping interval.
|
|
140
180
|
_clearIdle(entry);
|
|
141
181
|
_clearLiveness(entry);
|
|
182
|
+
// The entry is going away; keep its shard pin so the session's next socket
|
|
183
|
+
// can reconnect onto the same backend node.
|
|
184
|
+
_retainTurnState(poolKey, entry);
|
|
142
185
|
if (!poolKey) return;
|
|
143
186
|
const arr = _wsPool.get(poolKey);
|
|
144
187
|
if (!arr) return;
|
|
@@ -343,41 +386,24 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
|
|
|
343
386
|
'x-codex-beta-features': _codexBetaFeatures(),
|
|
344
387
|
};
|
|
345
388
|
const isOpenAiOauth = auth.type !== 'xai' && auth.type !== 'openai-direct';
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// to 13% after this header was dropped) all had it present. Send both.
|
|
352
|
-
// The underscore session_id is the backend prefix-dedupe key (2026-04-19
|
|
353
|
-
// probes; R15 regressed to 13% miss when it was dropped). Strict wire
|
|
354
|
-
// parity sends ONLY the dashed pair, but dropping this
|
|
355
|
-
// header is a KNOWN cache-unsafe change — so the general parity flag no
|
|
356
|
-
// longer silently drops it. Keep it unless an operator EXPLICITLY opts
|
|
357
|
-
// into the codex-exact dashed-only wire via
|
|
358
|
-
// MIXDOG_OAI_CODEX_WIRE_PARITY_SESSION_ID=dashed.
|
|
359
|
-
// The gate bundle implies the dashed-only wire (drop the underscore
|
|
360
|
-
// session_id) on top of the standalone opt-in.
|
|
361
|
-
const gate = _codexTurnStateGate();
|
|
362
|
-
const dropUnderscoreSessionId = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY_SESSION_ID === 'dashed' || gate;
|
|
363
|
-
if (sessionToken && !dropUnderscoreSessionId) {
|
|
389
|
+
// Default mode preserves the measured Mixdog underscore session_id cache
|
|
390
|
+
// key. Strict wire parity instead follows Codex exactly: dashed
|
|
391
|
+
// session-id/thread-id only, UUIDv7-shaped ids, and x-client-request-id.
|
|
392
|
+
const wireParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY === '1';
|
|
393
|
+
if (sessionToken && !wireParity) {
|
|
364
394
|
headers['session_id'] = String(sessionToken);
|
|
365
395
|
}
|
|
366
396
|
if (isOpenAiOauth && (sessionToken || _cacheKey)) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const uuidIds = _codexUuidIdParity() || gate;
|
|
370
|
-
const shapeId = gate ? _codexDashedIdV7 : _codexDashedId;
|
|
397
|
+
const uuidIds = wireParity;
|
|
398
|
+
const shapeId = wireParity ? _codexDashedIdV7 : _codexDashedId;
|
|
371
399
|
// Underscore `session_id` above is left as-is (backend prefix-dedupe
|
|
372
400
|
// key); only the dashed codex pair is reshaped under the opt-in.
|
|
373
401
|
const threadId = uuidIds ? shapeId(String(_cacheKey || sessionToken)) : String(_cacheKey || sessionToken);
|
|
374
402
|
const sessionId = uuidIds ? shapeId(String(sessionToken || _cacheKey)) : String(sessionToken || _cacheKey);
|
|
375
403
|
headers['session-id'] = sessionId;
|
|
376
404
|
headers['thread-id'] = threadId;
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
// turn-state issuance, so omit it entirely under the bundle.
|
|
380
|
-
if (!gate) headers['x-client-request-id'] = threadId;
|
|
405
|
+
// The reference client sets x-client-request-id to the dashed thread id.
|
|
406
|
+
headers['x-client-request-id'] = threadId;
|
|
381
407
|
if (codexHeaders && typeof codexHeaders === 'object') {
|
|
382
408
|
for (const [key, value] of Object.entries(codexHeaders)) {
|
|
383
409
|
if (typeof key === 'string' && typeof value === 'string' && value) {
|
|
@@ -580,6 +606,13 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
580
606
|
if (!_isOpen(arr[i]) || arr[i].closing || incompatibleIdle) {
|
|
581
607
|
if (incompatibleIdle) {
|
|
582
608
|
try { arr[i].socket.close(1000, 'pool_boundary_changed'); } catch {}
|
|
609
|
+
} else {
|
|
610
|
+
// A dead socket pruned here never reaches _removeFromPool,
|
|
611
|
+
// so retain its shard pin explicitly — this is the common
|
|
612
|
+
// reconnect path and the one the replacement handshake
|
|
613
|
+
// below reads. A boundary change deliberately drops the
|
|
614
|
+
// pin: it belongs to the old auth/cache identity.
|
|
615
|
+
_retainTurnState(poolKey, arr[i]);
|
|
583
616
|
}
|
|
584
617
|
_clearIdle(arr[i]);
|
|
585
618
|
_clearLiveness(arr[i]);
|
|
@@ -668,13 +701,17 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
668
701
|
// Parallel sockets must not inherit sibling turnState or the openai-oauth server
|
|
669
702
|
// treats the new request as a continuation of another in-flight turn and
|
|
670
703
|
// returns "No tool output found for function call …". turnState only
|
|
671
|
-
// propagates within a single entry across its own iterations
|
|
704
|
+
// propagates within a single entry across its own iterations — plus the
|
|
705
|
+
// codex reconnect case below, where the bucket holds no live entry and the
|
|
706
|
+
// retained token re-pins the shard the dead socket was using. forceFresh
|
|
707
|
+
// asks for a deliberately unpinned socket, so it always opens clean.
|
|
672
708
|
const sessionToken = _mintSessionToken(cacheKey, auth);
|
|
673
709
|
const compatibility = _poolCompatibility(auth, cacheKey);
|
|
710
|
+
const retainedTurnState = forceFresh ? null : _replacementTurnState(poolKey);
|
|
674
711
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
675
712
|
process.stderr.write(`[agent-trace] acquire-new tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} elapsed=${Date.now() - _acqStart}ms\n`);
|
|
676
713
|
}
|
|
677
|
-
const { socket, turnState } = await _openSocketImpl({ auth, sessionToken, turnState: null, externalSignal, cacheKey, codexHeaders });
|
|
714
|
+
const { socket, turnState } = await _openSocketImpl({ auth, sessionToken, turnState: retainedTurnState?.turnState || null, externalSignal, cacheKey, codexHeaders });
|
|
678
715
|
// Drain may complete while the normal handshake is awaiting 'open'. Never
|
|
679
716
|
// return or insert that late socket into the already-drained process pool.
|
|
680
717
|
if (_drainComplete) {
|
|
@@ -693,7 +730,11 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
|
|
|
693
730
|
lastInputPrefixHash: null,
|
|
694
731
|
releaseSequence: 0,
|
|
695
732
|
...compatibility,
|
|
696
|
-
|
|
733
|
+
// A token issued on THIS handshake supersedes the replayed one and is
|
|
734
|
+
// attributed on first use; a surviving retained token keeps its
|
|
735
|
+
// original turn owner so the per-turn drop guard still retires it.
|
|
736
|
+
turnState: turnState || retainedTurnState?.turnState || null,
|
|
737
|
+
turnStateTurnId: turnState ? null : (retainedTurnState?.turnStateTurnId ?? null),
|
|
697
738
|
closing: false,
|
|
698
739
|
ephemeral: false,
|
|
699
740
|
sessionToken,
|
|
@@ -733,10 +774,15 @@ export function releaseWebSocket({ entry, poolKey, keep }) {
|
|
|
733
774
|
|
|
734
775
|
export function closeOpenaiWsPoolForSession(poolKey, reason = 'session_closed') {
|
|
735
776
|
if (!poolKey) return;
|
|
777
|
+
// The session is over, so its shard pin must not survive into a later
|
|
778
|
+
// session that reuses the key. Clearing each entry's token before the
|
|
779
|
+
// close() calls also keeps the async 'close' handlers from re-retaining it.
|
|
780
|
+
_turnStateByPoolKey.delete(poolKey);
|
|
736
781
|
const entries = _wsPool.get(poolKey);
|
|
737
782
|
if (!entries) return;
|
|
738
783
|
_wsPool.delete(poolKey);
|
|
739
784
|
for (const entry of entries) {
|
|
785
|
+
entry.turnState = null;
|
|
740
786
|
_clearIdle(entry);
|
|
741
787
|
_clearLiveness(entry);
|
|
742
788
|
try { entry.socket.close(1000, reason); } catch {}
|
|
@@ -764,12 +810,14 @@ export function _seedWebSocketEntryForTest({ poolKey, auth, cacheKey, entry }) {
|
|
|
764
810
|
export function _clearWebSocketPoolForTest() {
|
|
765
811
|
for (const arr of _wsPool.values()) {
|
|
766
812
|
for (const entry of arr) {
|
|
813
|
+
entry.turnState = null;
|
|
767
814
|
_clearIdle(entry);
|
|
768
815
|
_clearLiveness(entry);
|
|
769
816
|
try { entry.socket.close(1000, 'test_cleanup'); } catch {}
|
|
770
817
|
}
|
|
771
818
|
}
|
|
772
819
|
_wsPool.clear();
|
|
820
|
+
_turnStateByPoolKey.clear();
|
|
773
821
|
_releaseSequence = 0;
|
|
774
822
|
}
|
|
775
823
|
|
|
@@ -180,6 +180,11 @@ function _hasHeaderKey(headers, name) {
|
|
|
180
180
|
|
|
181
181
|
export function _captureTurnStateFromEvent(entry, event) {
|
|
182
182
|
if (!entry || entry.turnState || !event || typeof event !== 'object') return;
|
|
183
|
+
// The token normally arrives on the WebSocket 101 handshake and is held
|
|
184
|
+
// write-once per entry, which is what keeps sticky routing stable across
|
|
185
|
+
// previous_response_id calls. This event path is the fallback for
|
|
186
|
+
// deployments that surface the header on a response event instead; the
|
|
187
|
+
// write-once guard above means it can never swap a live chain's token.
|
|
183
188
|
const turnState = _headerString(event.headers, X_CODEX_TURN_STATE_HEADER)
|
|
184
189
|
|| _headerString(event.response?.headers, X_CODEX_TURN_STATE_HEADER)
|
|
185
190
|
|| _headerString(event.response?.metadata?.headers, X_CODEX_TURN_STATE_HEADER)
|
|
@@ -354,6 +359,13 @@ export async function _streamResponse({
|
|
|
354
359
|
id: item.id || '',
|
|
355
360
|
encrypted_content: item.encrypted_content,
|
|
356
361
|
summary: Array.isArray(item.summary) ? item.summary : [],
|
|
362
|
+
// Emission order is part of the chain proof. A turn can reason,
|
|
363
|
+
// speak, then reason again before calling a tool; rebuilding it
|
|
364
|
+
// with every reasoning item in front of the text yields an order
|
|
365
|
+
// the previous response never produced, and the incremental check
|
|
366
|
+
// then fails on the message item and drops the chain to a cold
|
|
367
|
+
// full frame. Record which side of the text this item arrived on.
|
|
368
|
+
...(responseItemsAdded.some((added) => added?.type === 'message') ? { afterText: true } : {}),
|
|
357
369
|
});
|
|
358
370
|
};
|
|
359
371
|
const pushResponseItem = (item) => {
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
shouldExcludeIngestMessage,
|
|
38
38
|
} from '../../../../memory/lib/session-ingest.mjs';
|
|
39
39
|
import { cleanMemoryText } from '../../../../memory/lib/memory-extraction.mjs';
|
|
40
|
+
import { codexWireSendOpts } from '../manager/session-id.mjs';
|
|
40
41
|
import {
|
|
41
42
|
COMPACTION_SYSTEM_PROMPT,
|
|
42
43
|
fitCompactionPrompt,
|
|
@@ -446,6 +447,13 @@ export async function semanticCompactMessages(provider, messages, model, budgetT
|
|
|
446
447
|
if (opts.providerCacheKey || opts.sendOpts?.providerCacheKey) {
|
|
447
448
|
sendOpts.providerCacheKey = `${opts.providerCacheKey || opts.sendOpts.providerCacheKey}:compact`;
|
|
448
449
|
}
|
|
450
|
+
// Compaction is a request of the SAME session, not a session of its own:
|
|
451
|
+
// thread identity and window carry over, only the request kind and the turn
|
|
452
|
+
// id change. The `:compact` suffixes above stay — they scope the socket
|
|
453
|
+
// pool bucket so a summary never steals the live turn's socket — while the
|
|
454
|
+
// wire identity below keeps both on one server-side prefix-cache slot.
|
|
455
|
+
const codexWire = codexWireSendOpts(sendOpts.session, { requestKind: 'compaction' });
|
|
456
|
+
if (codexWire) Object.assign(sendOpts, codexWire);
|
|
449
457
|
|
|
450
458
|
const response = await provider.send([
|
|
451
459
|
{ role: 'system', content: COMPACTION_SYSTEM_PROMPT },
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
} from './runtime-liveness.mjs';
|
|
53
53
|
import { SessionClosedError } from './session-errors.mjs';
|
|
54
54
|
import { acquireSessionLock } from './session-lock.mjs';
|
|
55
|
+
import { codexWireSendOpts, ensureCodexWireSessionId, mintUuidV7 } from './session-id.mjs';
|
|
55
56
|
import { _tryBridgeExplicitPrefetch } from './prefetch-bridge.mjs';
|
|
56
57
|
import {
|
|
57
58
|
filterModelVisibleSessionMessages,
|
|
@@ -283,6 +284,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
283
284
|
if (preSession.closed === true) {
|
|
284
285
|
throw new SessionClosedError(sessionId, 'session already closed');
|
|
285
286
|
}
|
|
287
|
+
const _codexWireSessionId = ensureCodexWireSessionId(preSession);
|
|
286
288
|
if (!_crashRecoveryChecked) {
|
|
287
289
|
_crashRecoveryChecked = true;
|
|
288
290
|
const recovery = recoverTurnCheckpoint(preSession);
|
|
@@ -341,6 +343,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
341
343
|
const _turnInterruption = createTurnInterruptionTracker();
|
|
342
344
|
const _turnCheckpointToken = randomUUID();
|
|
343
345
|
const _turnCheckpointStartedAt = Date.now();
|
|
346
|
+
const _codexTurnId = _codexWireSessionId ? mintUuidV7(_turnCheckpointStartedAt) : null;
|
|
344
347
|
const _TURN_CHECKPOINT_THROTTLE_MS = 150;
|
|
345
348
|
let _turnCheckpointTimer = null;
|
|
346
349
|
let _turnCheckpointStopped = false;
|
|
@@ -736,6 +739,12 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
736
739
|
try { askOpts?.onSteerMessage?.(text, detail); } catch {}
|
|
737
740
|
},
|
|
738
741
|
notifyFn: typeof askOpts?.notifyFn === 'function' ? askOpts.notifyFn : undefined,
|
|
742
|
+
// Same projection compaction uses, so a session presents one
|
|
743
|
+
// identity on every request it makes.
|
|
744
|
+
...(codexWireSendOpts(session, {
|
|
745
|
+
turnId: _codexTurnId,
|
|
746
|
+
startedAtMs: _turnCheckpointStartedAt,
|
|
747
|
+
}) || {}),
|
|
739
748
|
promptCacheKey: session.promptCacheKey || sessionId,
|
|
740
749
|
// Provider-scoped cache key (mixdog-codex, mixdog-claude…).
|
|
741
750
|
// Distinct from sessionId — providers that pool sockets
|
|
@@ -349,6 +349,10 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
349
349
|
sessionId: resolvedSessionId,
|
|
350
350
|
cwd: session.cwd,
|
|
351
351
|
signal: opts.signal || null,
|
|
352
|
+
// Carries the session's wire identity so the
|
|
353
|
+
// summary request stays on the session's own
|
|
354
|
+
// prefix-cache slot instead of opening a second one.
|
|
355
|
+
sendOpts: { session },
|
|
352
356
|
promptCacheKey: session.promptCacheKey || null,
|
|
353
357
|
providerCacheKey: session.promptCacheKey || null,
|
|
354
358
|
timeoutMs: semanticCompactTimeoutMs(session, beforeMessageTokens),
|
|
@@ -394,6 +398,9 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
394
398
|
sessionId: resolvedSessionId,
|
|
395
399
|
cwd: session.cwd,
|
|
396
400
|
signal: opts.signal || null,
|
|
401
|
+
// Same-session wire identity: one prefix-cache slot for
|
|
402
|
+
// turns and their summaries alike.
|
|
403
|
+
sendOpts: { session },
|
|
397
404
|
promptCacheKey: session.promptCacheKey || null,
|
|
398
405
|
providerCacheKey: session.promptCacheKey || null,
|
|
399
406
|
timeoutMs: semanticCompactTimeoutMs(session, beforeMessageTokens),
|
|
@@ -4,6 +4,85 @@
|
|
|
4
4
|
// clearSessionMessages (clear-fork), matching the original single `nextId`.
|
|
5
5
|
import { randomBytes } from 'crypto';
|
|
6
6
|
let nextId = Date.now();
|
|
7
|
+
const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
8
|
+
let lastUuidV7Ms = -1;
|
|
9
|
+
let uuidV7Sequence = 0;
|
|
10
|
+
|
|
7
11
|
export function mintSessionId() {
|
|
8
12
|
return `sess_${process.pid}_${nextId++}_${Date.now()}_${randomBytes(16).toString('hex')}`;
|
|
9
13
|
}
|
|
14
|
+
|
|
15
|
+
export function isUuidV7(value) {
|
|
16
|
+
return UUID_V7_RE.test(String(value || '').trim());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function mintUuidV7(nowMs = Date.now()) {
|
|
20
|
+
const parsedNow = Number(nowMs);
|
|
21
|
+
const timestampMs = Math.max(
|
|
22
|
+
0,
|
|
23
|
+
Math.min(
|
|
24
|
+
0xffffffffffff,
|
|
25
|
+
Math.floor(Number.isFinite(parsedNow) ? parsedNow : Date.now()),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
if (timestampMs === lastUuidV7Ms) {
|
|
29
|
+
uuidV7Sequence = (uuidV7Sequence + 1) & 0x0fff;
|
|
30
|
+
} else {
|
|
31
|
+
lastUuidV7Ms = timestampMs;
|
|
32
|
+
uuidV7Sequence = randomBytes(2).readUInt16BE(0) & 0x0fff;
|
|
33
|
+
}
|
|
34
|
+
const bytes = randomBytes(16);
|
|
35
|
+
const timestamp = BigInt(timestampMs);
|
|
36
|
+
bytes[0] = Number((timestamp >> 40n) & 0xffn);
|
|
37
|
+
bytes[1] = Number((timestamp >> 32n) & 0xffn);
|
|
38
|
+
bytes[2] = Number((timestamp >> 24n) & 0xffn);
|
|
39
|
+
bytes[3] = Number((timestamp >> 16n) & 0xffn);
|
|
40
|
+
bytes[4] = Number((timestamp >> 8n) & 0xffn);
|
|
41
|
+
bytes[5] = Number(timestamp & 0xffn);
|
|
42
|
+
bytes[6] = 0x70 | ((uuidV7Sequence >> 8) & 0x0f);
|
|
43
|
+
bytes[7] = uuidV7Sequence & 0xff;
|
|
44
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
45
|
+
const hex = bytes.toString('hex');
|
|
46
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Send-opts projection of a session's Codex wire identity.
|
|
51
|
+
*
|
|
52
|
+
* Every request belonging to one session — live turns and compaction summaries
|
|
53
|
+
* alike — presents the SAME session/thread id, because the backend derives its
|
|
54
|
+
* prompt-cache slot from that id. Only `turn_id` and `request_kind` change per
|
|
55
|
+
* request. Splitting compaction onto its own identity would hand the same
|
|
56
|
+
* session two cache slots and cold-start one of them on every compaction.
|
|
57
|
+
*
|
|
58
|
+
* Returns null for providers without a Codex wire identity, so callers can
|
|
59
|
+
* spread the result unconditionally.
|
|
60
|
+
*/
|
|
61
|
+
export function codexWireSendOpts(session, { requestKind = 'turn', turnId = null, startedAtMs = null } = {}) {
|
|
62
|
+
const wireSessionId = ensureCodexWireSessionId(session);
|
|
63
|
+
if (!wireSessionId) return null;
|
|
64
|
+
const parsedStartedAt = Number(startedAtMs);
|
|
65
|
+
const startedAt = Number.isFinite(parsedStartedAt) && parsedStartedAt > 0
|
|
66
|
+
? Math.floor(parsedStartedAt)
|
|
67
|
+
: Date.now();
|
|
68
|
+
return {
|
|
69
|
+
codexSessionId: wireSessionId,
|
|
70
|
+
codexThreadId: wireSessionId,
|
|
71
|
+
turnId: turnId || mintUuidV7(startedAt),
|
|
72
|
+
windowId: `${wireSessionId}:0`,
|
|
73
|
+
turnStartedAtUnixMs: startedAt,
|
|
74
|
+
requestKind,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ensureCodexWireSessionId(session) {
|
|
79
|
+
if (!session || session.provider !== 'openai-oauth') return null;
|
|
80
|
+
const current = String(session.codexWireSessionId || '').trim().toLowerCase();
|
|
81
|
+
if (isUuidV7(current)) {
|
|
82
|
+
session.codexWireSessionId = current;
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
const minted = mintUuidV7();
|
|
86
|
+
session.codexWireSessionId = minted;
|
|
87
|
+
return minted;
|
|
88
|
+
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import {
|
|
30
30
|
filterModelEditToolNames,
|
|
31
31
|
filterModelEditTools,
|
|
32
|
+
unusedModelEditToolName,
|
|
32
33
|
} from '../../../../shared/edit-tool-dialect.mjs';
|
|
33
34
|
import {
|
|
34
35
|
positiveContextWindow,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
resolveSessionContextMeta,
|
|
37
38
|
} from './context-meta.mjs';
|
|
38
39
|
import { getAgentRuntimeSync, warnAgentRuntimeResolveFailureOnce } from './agent-runtime-singleton.mjs';
|
|
39
|
-
import { mintSessionId } from './session-id.mjs';
|
|
40
|
+
import { ensureCodexWireSessionId, mintSessionId, mintUuidV7 } from './session-id.mjs';
|
|
40
41
|
import { providerCacheKey } from './provider-cache-key.mjs';
|
|
41
42
|
import { clearTurnCheckpoint, recoverTurnCheckpoint } from './turn-checkpoint.mjs';
|
|
42
43
|
import {
|
|
@@ -234,7 +235,11 @@ export function createSession(opts) {
|
|
|
234
235
|
...(hiddenAgent ? ['Skill'] : []),
|
|
235
236
|
...(!ownerIsAgent && workflowDisallowsAgentTool(opts.workflow) ? ['agent'] : []),
|
|
236
237
|
];
|
|
237
|
-
|
|
238
|
+
// The edit dialect this model never receives is omitted from the rules as
|
|
239
|
+
// well, so a session never reads placement guidance for a tool it cannot
|
|
240
|
+
// call.
|
|
241
|
+
const ruleOmitTools = [...sessionDeny, unusedModelEditToolName(modelName)];
|
|
242
|
+
const injectedRules = skipAgentRules ? '' : _buildSharedRules({ omitTools: ruleOmitTools });
|
|
238
243
|
const delegationFree = !ownerIsAgent && workflowDisallowsAgentTool(opts.workflow);
|
|
239
244
|
const roleRules = skipAgentRules
|
|
240
245
|
? ''
|
|
@@ -385,6 +390,7 @@ export function createSession(opts) {
|
|
|
385
390
|
});
|
|
386
391
|
const session = {
|
|
387
392
|
id,
|
|
393
|
+
codexWireSessionId: providerName === 'openai-oauth' ? mintUuidV7() : null,
|
|
388
394
|
provider: providerName,
|
|
389
395
|
model: modelName,
|
|
390
396
|
messages,
|
|
@@ -495,6 +501,7 @@ export function updateSessionRoute(id, route = {}) {
|
|
|
495
501
|
if (route.model) session.model = route.model;
|
|
496
502
|
if (Object.prototype.hasOwnProperty.call(route, 'fast')) session.fast = route.fast === true;
|
|
497
503
|
if (Object.prototype.hasOwnProperty.call(route, 'effort')) session.effort = route.effort || null;
|
|
504
|
+
ensureCodexWireSessionId(session);
|
|
498
505
|
if (Object.prototype.hasOwnProperty.call(route, 'modelParameters')) {
|
|
499
506
|
session.modelParameters = route.modelParameters && typeof route.modelParameters === 'object'
|
|
500
507
|
? { ...route.modelParameters }
|
|
@@ -809,6 +816,7 @@ export async function resumeSession(sessionId, preset, options = {}) {
|
|
|
809
816
|
// save below would also block the write, but failing fast here is cleaner
|
|
810
817
|
// than silently dropping the tool-refresh side effects.
|
|
811
818
|
if (session.closed === true) return null;
|
|
819
|
+
ensureCodexWireSessionId(session);
|
|
812
820
|
// Desktop callers pass their selected durable classification as a
|
|
813
821
|
// capability check. Refuse a stale/tampered cross-class resume before any
|
|
814
822
|
// tool refresh or save. CLI/TUI callers omit this option and retain the
|
|
@@ -14,8 +14,8 @@ const _shellSyntaxCheat =
|
|
|
14
14
|
// Keep the routing map short and adjacent to the shell's primary description.
|
|
15
15
|
// PowerShell aliases appear only on win32.
|
|
16
16
|
const _shellToolRouting = process.platform === 'win32'
|
|
17
|
-
? 'Use read, NOT cat/Get-Content/head/tail; list, NOT ls/dir; find/glob, NOT find; grep, NOT grep/rg/Select-String; edit/apply_patch, NOT sed/awk/echo/Set-Content or a file-writing heredoc.'
|
|
18
|
-
: 'Use read, NOT cat/head/tail; list, NOT ls; find/glob, NOT find; grep, NOT grep/rg; edit/apply_patch, NOT sed/awk/echo or a file-writing heredoc.';
|
|
17
|
+
? 'Use read, NOT cat/Get-Content/head/tail; list, NOT ls/dir; find/glob, NOT find; grep, NOT grep/rg/Select-String; edit/apply_patch, NOT sed/awk/echo/Set-Content or a file-writing heredoc; git, NOT a plain git command; task, NOT Start-Job or a wait loop.'
|
|
18
|
+
: 'Use read, NOT cat/head/tail; list, NOT ls; find/glob, NOT find; grep, NOT grep/rg; edit/apply_patch, NOT sed/awk/echo or a file-writing heredoc; git, NOT a plain git command; task, NOT & or a wait loop.';
|
|
19
19
|
// Process-stable switch used to describe foreground-only execution accurately.
|
|
20
20
|
const _shellBackgroundDisabled = /^(1|true|yes|on)$/i.test(
|
|
21
21
|
String(process.env.MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS || '').trim(),
|
|
@@ -26,7 +26,7 @@ export const BUILTIN_TOOLS = [
|
|
|
26
26
|
name: 'read',
|
|
27
27
|
title: 'Read',
|
|
28
28
|
annotations: { title: 'Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
29
|
-
description: 'Read-only; safe to batch in parallel. Known-file contents or line ranges, bounded to the narrowest range that answers the question.
|
|
29
|
+
description: 'Read-only; safe to batch in parallel. Known-file contents or line ranges, bounded to the narrowest range that answers the question. Images render for viewing; not directories. Replaces cat/head/tail.',
|
|
30
30
|
inputSchema: {
|
|
31
31
|
type: 'object',
|
|
32
32
|
properties: {
|
|
@@ -52,7 +52,7 @@ export const BUILTIN_TOOLS = [
|
|
|
52
52
|
name: 'edit',
|
|
53
53
|
title: 'Edit',
|
|
54
54
|
annotations: { title: 'Edit', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
55
|
-
description: 'Replace exact text in one file. Use exact text already in context; never re-open the file to build old_string or to verify a successful edit. old_string must match once unless replace_all is true. Empty old_string creates a missing file or fills an empty file; it never overwrites a non-empty file.',
|
|
55
|
+
description: 'Replace exact text in one file. Use exact text already in context; never re-open the file to build old_string or to verify a successful edit. old_string must match once unless replace_all is true. Empty old_string creates a missing file or fills an empty file; it never overwrites a non-empty file. Replaces sed/awk and echo redirection.',
|
|
56
56
|
inputSchema: {
|
|
57
57
|
type: 'object',
|
|
58
58
|
properties: {
|
|
@@ -81,7 +81,7 @@ export const BUILTIN_TOOLS = [
|
|
|
81
81
|
name: 'shell',
|
|
82
82
|
title: 'Shell',
|
|
83
83
|
annotations: { title: 'Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
84
|
-
description: `Run programs, runtime/state operations, calculations, transformations, file generation, and unsupported-format inspection. ${_shellToolRouting}
|
|
84
|
+
description: `Run programs, runtime/state operations, calculations, transformations, file generation, and unsupported-format inspection. ${_shellToolRouting} ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'Commands use a 10s foreground window by default—not a timeout. Still-running work continues as a tracked task_id. Completion is automatic; do not call task read/monitor to wait or check progress. Continue independent work or end the turn. Use task read only when the user explicitly asks for current status, and task monitor only when they explicitly ask for periodic progress.'}`,
|
|
85
85
|
inputSchema: {
|
|
86
86
|
type: 'object',
|
|
87
87
|
properties: {
|
|
@@ -102,7 +102,7 @@ export const BUILTIN_TOOLS = [
|
|
|
102
102
|
name: 'task',
|
|
103
103
|
title: 'Task',
|
|
104
104
|
annotations: { title: 'Task', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
105
|
-
description: 'List shell tasks, read one current snapshot, change periodic monitoring, or cancel by task_id. Completion is automatic; do not
|
|
105
|
+
description: 'List shell tasks, read one current snapshot, change periodic monitoring, or cancel by task_id. Replaces shell job control (jobs/wait/Start-Job). Completion is automatic; do not call read/monitor to wait or check progress. Continue independent work or end the turn. Use read only when the user explicitly asks for current status, and monitor only when they explicitly ask for periodic progress.',
|
|
106
106
|
inputSchema: {
|
|
107
107
|
type: 'object',
|
|
108
108
|
properties: {
|
|
@@ -153,7 +153,7 @@ export const BUILTIN_TOOLS = [
|
|
|
153
153
|
name: 'glob',
|
|
154
154
|
title: 'Glob',
|
|
155
155
|
annotations: { title: 'Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
156
|
-
description: 'Read-only; safe to batch in parallel. Return wildcard-matching file paths under a known base directory when those paths are needed. Omit path for the current Project;
|
|
156
|
+
description: 'Read-only; safe to batch in parallel. Return wildcard-matching file paths under a known base directory when those paths are needed. Omit path for the current Project; an unknown base directory goes to find first. Directories never match. Newest first by default. Replaces find -name.',
|
|
157
157
|
inputSchema: {
|
|
158
158
|
type: 'object',
|
|
159
159
|
properties: {
|
|
@@ -165,7 +165,7 @@ export const BUILTIN_TOOLS = [
|
|
|
165
165
|
description: 'Glob or array of globs (max 10).',
|
|
166
166
|
},
|
|
167
167
|
path: {
|
|
168
|
-
type: 'string', description: 'Known existing base directory; omit for the current Project
|
|
168
|
+
type: 'string', description: 'Known existing base directory; omit for the current Project.',
|
|
169
169
|
},
|
|
170
170
|
sort: { type: 'string', enum: ['natural', 'mtime'], description: 'mtime default (newest first); natural = raw walk order, cheaper on huge trees.' },
|
|
171
171
|
limit: { type: 'integer', minimum: 0, description: 'Max entries; default 100; 0 unlimited.' },
|
|
@@ -198,7 +198,7 @@ export const BUILTIN_TOOLS = [
|
|
|
198
198
|
name: 'list',
|
|
199
199
|
title: 'List Directory',
|
|
200
200
|
annotations: { title: 'List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
201
|
-
description: "Read-only; safe to batch in parallel. Return a known directory's immediate entries (path + type) when the entry list itself is needed; never as a prerequisite for another tool on that directory. No wildcard; meta:true adds size/mtime/mode.",
|
|
201
|
+
description: "Read-only; safe to batch in parallel. Return a known directory's immediate entries (path + type) when the entry list itself is needed; never as a prerequisite for another tool on that directory. No wildcard; meta:true adds size/mtime/mode. Replaces ls/dir.",
|
|
202
202
|
inputSchema: {
|
|
203
203
|
type: 'object',
|
|
204
204
|
properties: {
|
|
@@ -13,27 +13,39 @@ function summarizeJobPreviewText(text, maxChars = 160) {
|
|
|
13
13
|
return summary.length > maxChars ? `${summary.slice(0, maxChars - 1)}…` : summary;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// The reported source must be the stream the summary actually came from: a
|
|
17
|
+
// present-but-blank preview previously mislabeled a stderr summary as stdout.
|
|
18
|
+
function pickJobSummary(candidates) {
|
|
19
|
+
for (const [source, text] of candidates) {
|
|
20
|
+
const summary = summarizeJobPreviewText(text);
|
|
21
|
+
if (summary) return { summary, summarySource: source };
|
|
22
|
+
}
|
|
23
|
+
return { summary: '', summarySource: '' };
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export function attachJobInsights(detail) {
|
|
17
27
|
if (!detail || typeof detail !== 'object') return detail;
|
|
18
28
|
const result = { ...detail };
|
|
19
29
|
let summary = '';
|
|
20
30
|
let summarySource = '';
|
|
31
|
+
const stdoutFirst = [['stdout', result.stdoutPreview], ['stderr', result.stderrPreview]];
|
|
32
|
+
const stderrFirst = [['stderr', result.stderrPreview], ['stdout', result.stdoutPreview]];
|
|
21
33
|
if (result.status === 'completed') {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
// A non-zero exit means the diagnosis lives in stderr, so lead with it
|
|
35
|
+
// instead of the last stdout line, which is usually unrelated progress.
|
|
36
|
+
const failedExit = typeof result.exitCode === 'number' && result.exitCode !== 0;
|
|
37
|
+
({ summary, summarySource } = pickJobSummary(failedExit ? stderrFirst : stdoutFirst));
|
|
25
38
|
} else if (result.status === 'failed') {
|
|
26
|
-
summary =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
({ summary, summarySource } = pickJobSummary(stderrFirst));
|
|
40
|
+
if (!summary) {
|
|
41
|
+
summary = String(result.error || '').trim();
|
|
42
|
+
summarySource = summary ? 'status' : '';
|
|
43
|
+
}
|
|
30
44
|
} else if (result.status === 'cancelled') {
|
|
31
45
|
summary = 'cancelled before completion';
|
|
32
46
|
summarySource = 'status';
|
|
33
47
|
} else if (result.status === 'running') {
|
|
34
|
-
summary =
|
|
35
|
-
|| summarizeJobPreviewText(result.stderrPreview);
|
|
36
|
-
summarySource = summary ? (result.stdoutPreview ? 'stdout' : 'stderr') : '';
|
|
48
|
+
({ summary, summarySource } = pickJobSummary(stdoutFirst));
|
|
37
49
|
}
|
|
38
50
|
if (summary) {
|
|
39
51
|
result.summary = summary;
|
|
@@ -76,8 +76,32 @@ function compactStructuredJson(stdout) {
|
|
|
76
76
|
return `${compacted.join('\n')}${hadTrailingNewline ? '\n' : ''}`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// A test summary states the RUN's verdict, so it may only replace output whose
|
|
80
|
+
// exit status belongs to the runner itself. A pipeline, chain, or substitution
|
|
81
|
+
// can hand back someone else's status (`node --test … | tail` exits 0 while the
|
|
82
|
+
// suite fails), which would turn a failed run into a success line. Plain
|
|
83
|
+
// redirects keep the runner's own status and stay eligible.
|
|
84
|
+
function statusOwningCommand(command) {
|
|
85
|
+
const text = String(command ?? '').replace(/\d?>&\d/g, ' ').trim();
|
|
86
|
+
if (!text) return false;
|
|
87
|
+
return !/[;&|<>`]|\$\(|\$\{/.test(text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Runner-agnostic failure markers. Whichever matcher would otherwise claim the
|
|
91
|
+
// run, an explicit failure line forbids a success summary.
|
|
92
|
+
function hasTestFailureMarker(stdout) {
|
|
93
|
+
const text = String(stdout ?? '');
|
|
94
|
+
return /(?:^|\n)\s*not ok\b/i.test(text)
|
|
95
|
+
|| /^#\s*(?:fail|cancelled|todo)\s+[1-9]/m.test(text)
|
|
96
|
+
|| /(?:^|\n)(?:---\s*FAIL|FAIL\b|FAILED\b)/.test(text)
|
|
97
|
+
|| /(?:^|\n)\s*failures:/i.test(text)
|
|
98
|
+
|| /\bpanicked at\b/.test(text)
|
|
99
|
+
|| /[\u2716\u2718]/.test(text);
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
function testLikeCommand(command) {
|
|
80
103
|
const text = String(command ?? '').trim();
|
|
104
|
+
if (!statusOwningCommand(text)) return false;
|
|
81
105
|
return simplePytestCommand(text)
|
|
82
106
|
|| /^(?:node|bun)(?:\.exe)?\s+--test(?:\s|$)/i.test(text)
|
|
83
107
|
|| /^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test(?:[:.\w-]*)(?:\s|$)/i.test(text)
|
|
@@ -137,10 +161,15 @@ function cargoTestSuccessSummary(stdout) {
|
|
|
137
161
|
return `Cargo test: ${passed} passed in ${resultLines.length} suites${duration ? ` (${duration.toFixed(2)}s)` : ''}`;
|
|
138
162
|
}
|
|
139
163
|
|
|
164
|
+
// Go prints `ok <pkg> 0.02s` or `ok <pkg> (cached)`. Anchoring on that
|
|
165
|
+
// trailing duration field keeps TAP's `ok 3 - name` result lines — which are
|
|
166
|
+
// individual assertions, not packages — from being counted as Go packages.
|
|
140
167
|
function goTestSuccessSummary(stdout) {
|
|
141
168
|
const text = String(stdout ?? '');
|
|
142
169
|
if (/(?:^|\n)(?:FAIL\b|--- FAIL:)|\b(?:panic|warning):/im.test(text)) return null;
|
|
143
|
-
|
|
170
|
+
if (/^TAP version \d+/m.test(text) || /^1\.\.\d+\s*$/m.test(text)) return null;
|
|
171
|
+
const packages = text.split(/\r?\n/)
|
|
172
|
+
.filter((line) => /^ok\s+\S+\s+(?:\(cached\)|[\d.]+m?s)\b/.test(line.trim()));
|
|
144
173
|
if (!packages.length) return null;
|
|
145
174
|
return `Go test: ${packages.length} packages passed`;
|
|
146
175
|
}
|
|
@@ -3,7 +3,7 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
3
3
|
name: 'code_graph',
|
|
4
4
|
title: 'Code Graph',
|
|
5
5
|
annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
6
|
-
description: 'Read-only; safe to batch in parallel. Source-file structure, symbol relations, and flow. Exact identifiers route directly to find_symbol/references/callers/callees; symbol-name keywords use symbol_search/search.
|
|
6
|
+
description: 'Read-only; safe to batch in parallel. Source-file structure, symbol relations, and flow. Exact identifiers route directly to find_symbol/references/callers/callees; symbol-name keywords use symbol_search/search. Text, literals, and regex belong to grep. File modes use files[]; symbol modes use symbols[]. overview and mode:symbols return file summaries. find_symbol returns declaration/body; references returns declaration/usages plus optional body; callers/callees return locations.',
|
|
7
7
|
inputSchema: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|