mixdog 0.9.96 → 0.9.99
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/README.md +0 -17
- package/package.json +16 -8
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +43 -32
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
- package/src/runtime/channels/lib/worker-main.mjs +39 -58
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/http-wire.mjs +39 -4
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/json-metrics.mjs +94 -0
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +30 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +12 -2
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +61 -32
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +28 -4
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +9 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +15 -17
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +403 -170
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +28 -14
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
- package/src/tui/{engine → session}/session-api.mjs +17 -61
- package/src/tui/{engine → session}/session-flow.mjs +5 -3
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -9,6 +9,7 @@ import { readStreamOutcome } from '../../providers/lib/stream-outcome.mjs';
|
|
|
9
9
|
import { normalizeCompactType, DEFAULT_COMPACT_TYPE } from '../compact.mjs';
|
|
10
10
|
import { loadSession, saveSession, saveSessionAsync, saveSessionAsyncDeferred, readSessionLifecycleFromDisk } from '../store.mjs';
|
|
11
11
|
import { createAbortController } from '../../../../shared/abort-controller.mjs';
|
|
12
|
+
import { estimateJsonBytes } from '../../../../shared/json-metrics.mjs';
|
|
12
13
|
import { logLlmCall } from '../../../../shared/llm/usage-log.mjs';
|
|
13
14
|
import { appendAgentTrace } from '../../agent-trace.mjs';
|
|
14
15
|
import { recordStandaloneStatusTelemetry } from './status-telemetry.mjs';
|
|
@@ -67,6 +68,17 @@ import {
|
|
|
67
68
|
import { _getAgentLoop } from './runtime-loaders.mjs';
|
|
68
69
|
import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
|
|
69
70
|
import { recordProviderContextBaseline } from '../loop/compact-policy.mjs';
|
|
71
|
+
import { runAbortable, settleWithin, throwIfAborted } from '../../../../shared/abort-race.mjs';
|
|
72
|
+
|
|
73
|
+
export const DEFAULT_ASK_CLEANUP_SETTLE_MS = 2_000;
|
|
74
|
+
|
|
75
|
+
export async function settleAskCleanup(promise, { timeoutMs } = {}) {
|
|
76
|
+
const configured = Number(process.env.MIXDOG_ASK_CLEANUP_SETTLE_MS);
|
|
77
|
+
const budget = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
|
|
78
|
+
? Number(timeoutMs)
|
|
79
|
+
: (Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_ASK_CLEANUP_SETTLE_MS);
|
|
80
|
+
return await settleWithin(promise, budget);
|
|
81
|
+
}
|
|
70
82
|
|
|
71
83
|
export function persistedAssistantTranscriptMetadata(value, fallbackAt = Date.now()) {
|
|
72
84
|
if (!value || typeof value !== 'object') return null;
|
|
@@ -196,7 +208,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
196
208
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
197
209
|
process.stderr.write(`[agent-trace] t0-ask-start sessionHash=${createHash('sha256').update(String(sessionId)).digest('hex').slice(0, 8)} role=? iteration=0 promptSrc=${_promptSrc} prefetchFiles=${_prefetchFiles} callers=${_prefetchCallers} references=${_prefetchRefs}\n`);
|
|
198
210
|
}
|
|
199
|
-
const unlock = await acquireSessionLock(sessionId);
|
|
211
|
+
const unlock = await acquireSessionLock(sessionId, askOpts?.signal);
|
|
200
212
|
// Start crash-spool hydration without delaying the user turn. A completed
|
|
201
213
|
// background hydration joins the id-deduped memory drain below.
|
|
202
214
|
const takeoverHydration = hydratePendingMessages(sessionId);
|
|
@@ -309,9 +321,10 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
309
321
|
// computation would run detached. Capture the linked signal, install the
|
|
310
322
|
// fresh controller, then re-cascade it — aborting the new controller
|
|
311
323
|
// immediately when the parent already fired, or re-arming the listener.
|
|
312
|
-
const _linkedParentSignal = runtime.parentAbortLink?.signal;
|
|
324
|
+
const _linkedParentSignal = askOpts?.signal || runtime.parentAbortLink?.signal;
|
|
313
325
|
// Fresh controller per ask — the previous ask's controller may have aborted.
|
|
314
326
|
runtime.controller = createAbortController();
|
|
327
|
+
const turnSignal = runtime.controller.signal;
|
|
315
328
|
runtime.generation = askGeneration;
|
|
316
329
|
runtime.closed = false;
|
|
317
330
|
runtime.session = preSession;
|
|
@@ -469,7 +482,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
469
482
|
// even to the real task. Single-turn structure with a labelled
|
|
470
483
|
// `# Task` block forces the model to treat the brief as the work
|
|
471
484
|
// unit, not as another piece of context to ack.
|
|
472
|
-
const explicitPrefetchResult = await _tryBridgeExplicitPrefetch(session, explicitPrefetch);
|
|
485
|
+
const explicitPrefetchResult = await _tryBridgeExplicitPrefetch(session, explicitPrefetch, turnSignal);
|
|
473
486
|
let _contextBlock = '';
|
|
474
487
|
if (context) {
|
|
475
488
|
_contextBlock += `# Additional context\n${_capCtx(context)}\n\n`;
|
|
@@ -556,7 +569,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
556
569
|
const _prefetchBytes = Buffer.byteLength(explicitPrefetchResult || '', 'utf8');
|
|
557
570
|
const _promptBytes = promptContentBytes(prompt);
|
|
558
571
|
const _userTurnBytes = promptContentBytes(_userTurnContent);
|
|
559
|
-
const _messagesBytes =
|
|
572
|
+
const _messagesBytes = estimateJsonBytes(historyMessages || []);
|
|
560
573
|
const _totalBytes = _userTurnBytes + _messagesBytes;
|
|
561
574
|
appendAgentTrace({
|
|
562
575
|
kind: 'context',
|
|
@@ -574,7 +587,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
574
587
|
},
|
|
575
588
|
});
|
|
576
589
|
} catch { /* trace must never break the ask path */ }
|
|
577
|
-
const agentLoop = await _getAgentLoop();
|
|
590
|
+
const agentLoop = await runAbortable(turnSignal, () => _getAgentLoop());
|
|
578
591
|
const _trackTextDelta = (chunk) => {
|
|
579
592
|
_turnInterruption.recordTextDelta(chunk);
|
|
580
593
|
_scheduleTurnCheckpoint();
|
|
@@ -766,6 +779,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
766
779
|
session.toolApprovalHook = priorToolApprovalHook;
|
|
767
780
|
}
|
|
768
781
|
}
|
|
782
|
+
throwIfAborted(turnSignal);
|
|
769
783
|
// Post-loop validation: if closeSession() landed while we were awaiting,
|
|
770
784
|
// drop the save so the tombstone on disk isn't overwritten.
|
|
771
785
|
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
@@ -900,7 +914,10 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
900
914
|
// Unified total-prompt field. Anthropic = input+cache_read+cache_write
|
|
901
915
|
// (additive); OpenAI OAuth/API/Gemini = input_tokens already includes the
|
|
902
916
|
// cached portion (inclusive), so the fallback must not double-count.
|
|
903
|
-
const { isInclusiveProvider, computeCostUsd } = await
|
|
917
|
+
const { isInclusiveProvider, computeCostUsd } = await runAbortable(
|
|
918
|
+
turnSignal,
|
|
919
|
+
() => import('../../../../shared/llm/cost.mjs'),
|
|
920
|
+
);
|
|
904
921
|
const inclusive = isInclusiveProvider(session.provider);
|
|
905
922
|
const promptTokens = typeof result.usage.promptTokens === 'number'
|
|
906
923
|
? result.usage.promptTokens
|
|
@@ -1042,15 +1059,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1042
1059
|
}
|
|
1043
1060
|
try {
|
|
1044
1061
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1045
|
-
|
|
1046
|
-
|
|
1062
|
+
const cleanup = finalized.responsePreserved
|
|
1063
|
+
? finalizePendingMessageDelivery(
|
|
1047
1064
|
activeSession, _turnPendingEntries, durableSave,
|
|
1048
1065
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1049
|
-
)
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
1066
|
+
)
|
|
1067
|
+
: durableSave;
|
|
1068
|
+
const cleanupResult = await settleAskCleanup(cleanup);
|
|
1069
|
+
if (cleanupResult.settled) clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
1054
1070
|
} catch { /* cancellation cleanup is best-effort */ }
|
|
1055
1071
|
if (currentRuntime) currentRuntime.session = activeSession;
|
|
1056
1072
|
} else releasePendingMessages(sessionId, _turnPendingEntries);
|
|
@@ -1088,35 +1104,34 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1088
1104
|
else releasePendingMessages(sessionId, _turnPendingEntries);
|
|
1089
1105
|
try {
|
|
1090
1106
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1091
|
-
|
|
1092
|
-
|
|
1107
|
+
const cleanup = finalized.responsePreserved
|
|
1108
|
+
? finalizePendingMessageDelivery(
|
|
1093
1109
|
activeSession, _turnPendingEntries, durableSave,
|
|
1094
1110
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1095
|
-
)
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}
|
|
1099
|
-
_errorStateDurable = true;
|
|
1111
|
+
)
|
|
1112
|
+
: durableSave;
|
|
1113
|
+
_errorStateDurable = (await settleAskCleanup(cleanup)).settled;
|
|
1100
1114
|
} catch { /* provider-failure history persistence is best-effort */ }
|
|
1101
1115
|
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
1102
1116
|
if (currentRuntime) currentRuntime.session = activeSession;
|
|
1103
1117
|
} else {
|
|
1104
|
-
const
|
|
1118
|
+
const compactPersist = await settleAskCleanup(persistCompactedOutgoingAfterAskFailure({
|
|
1105
1119
|
sessionId,
|
|
1106
1120
|
activeSession,
|
|
1107
1121
|
askGeneration,
|
|
1108
1122
|
turnOutgoing: _turnOutgoing,
|
|
1109
1123
|
error: err,
|
|
1110
|
-
});
|
|
1124
|
+
}));
|
|
1125
|
+
const promptPersisted = compactPersist.settled && compactPersist.value === true;
|
|
1111
1126
|
if (promptPersisted) {
|
|
1112
1127
|
recordPendingMessageDelivery(activeSession, _turnPendingEntries);
|
|
1113
1128
|
try {
|
|
1114
1129
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1115
|
-
|
|
1130
|
+
const cleanup = finalizePendingMessageDelivery(
|
|
1116
1131
|
activeSession, _turnPendingEntries, durableSave,
|
|
1117
1132
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1118
1133
|
);
|
|
1119
|
-
_errorStateDurable =
|
|
1134
|
+
_errorStateDurable = (await settleAskCleanup(cleanup)).settled;
|
|
1120
1135
|
} catch {}
|
|
1121
1136
|
} else {
|
|
1122
1137
|
releasePendingMessages(sessionId, _turnPendingEntries);
|
|
@@ -1124,8 +1139,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1124
1139
|
}
|
|
1125
1140
|
if (!_errorStateDurable && activeSession) {
|
|
1126
1141
|
try {
|
|
1127
|
-
await
|
|
1128
|
-
|
|
1142
|
+
_errorStateDurable = (await settleAskCleanup(
|
|
1143
|
+
saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1144
|
+
)).settled;
|
|
1129
1145
|
} catch { /* retain checkpoint when canonical persistence fails */ }
|
|
1130
1146
|
}
|
|
1131
1147
|
if (_errorStateDurable) clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
@@ -77,8 +77,7 @@ function addCompactUsageToSession(session, usage) {
|
|
|
77
77
|
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
78
78
|
session.tokensCumulative = (session.tokensCumulative || 0) + inputTokens + outputTokens;
|
|
79
79
|
}
|
|
80
|
-
// A
|
|
81
|
-
// and retry once (memory-runtime-proxy.mjs) — that must NEVER wedge the
|
|
80
|
+
// A stalled memory call must NEVER wedge the
|
|
82
81
|
// compact//clear path. Bound every recall-fasttrack memory call with a short
|
|
83
82
|
// local timeout: on timeout we abort (best-effort cancel via a chained signal)
|
|
84
83
|
// and treat it exactly like an RPC failure, so compaction proceeds WITHOUT
|
|
@@ -93,7 +92,7 @@ function recallMemoryTimeoutMs(session) {
|
|
|
93
92
|
// misconfigured tiny value can't turn the bound into a busy no-wait.
|
|
94
93
|
return Math.max(250, configured || RECALL_MEMORY_CALL_TIMEOUT_MS);
|
|
95
94
|
}
|
|
96
|
-
// Cold-start allowance (clear/manual path only): a booting memory
|
|
95
|
+
// Cold-start allowance (clear/manual path only): a booting memory runtime can
|
|
97
96
|
// miss the tight first bound (waitForPort + first-RPC warmup ~2-10s). On a
|
|
98
97
|
// timeout we retry ONCE with a longer bound before honoring the bail-to-
|
|
99
98
|
// semantic contract, so a rebooting runtime succeeds instead of instantly
|
|
@@ -355,6 +355,9 @@ function normalizePendingMessageEntry(entry) {
|
|
|
355
355
|
id: pendingMessageId(entry),
|
|
356
356
|
enqueuedAt: Number(entry.enqueuedAt) || Date.now(),
|
|
357
357
|
};
|
|
358
|
+
const entryOptions = entry.options && typeof entry.options === 'object'
|
|
359
|
+
? { options: entry.options }
|
|
360
|
+
: {};
|
|
358
361
|
const marker = entry.notificationKind === COMPLETION_NOTIFICATION_KIND
|
|
359
362
|
? { notificationKind: COMPLETION_NOTIFICATION_KIND, enqueuedAt: Number(entry.enqueuedAt) || Date.now() }
|
|
360
363
|
: null;
|
|
@@ -366,13 +369,13 @@ function normalizePendingMessageEntry(entry) {
|
|
|
366
369
|
if (content == null) return null;
|
|
367
370
|
const text = typeof entry.text === 'string' ? entry.text.trim() : promptContentText(content).trim();
|
|
368
371
|
let out = null;
|
|
369
|
-
if (Array.isArray(content)) out = content.length > 0 ? { content, text } : null;
|
|
372
|
+
if (Array.isArray(content)) out = content.length > 0 ? { content, text, ...entryOptions } : null;
|
|
370
373
|
else if (typeof content === 'string') {
|
|
371
374
|
const value = content.trim();
|
|
372
|
-
out = value ? { content: value, text: text || value } : null;
|
|
375
|
+
out = value ? { content: value, text: text || value, ...entryOptions } : null;
|
|
373
376
|
} else {
|
|
374
377
|
const fallback = promptContentText(content).trim();
|
|
375
|
-
out = fallback ? { content: fallback, text: text || fallback } : null;
|
|
378
|
+
out = fallback ? { content: fallback, text: text || fallback, ...entryOptions } : null;
|
|
376
379
|
}
|
|
377
380
|
if (!out) return null;
|
|
378
381
|
// The lifecycle token rides along every derived copy (see carryLifecycleToken).
|
|
@@ -394,7 +397,12 @@ function pendingMessageQueueEntry(entry) {
|
|
|
394
397
|
const marker = isCompletionNotificationEntry(normalized)
|
|
395
398
|
? { notificationKind: COMPLETION_NOTIFICATION_KIND, enqueuedAt: normalized.enqueuedAt }
|
|
396
399
|
: null;
|
|
397
|
-
const base = {
|
|
400
|
+
const base = {
|
|
401
|
+
...identity,
|
|
402
|
+
content: normalized.content,
|
|
403
|
+
text: normalized.text || promptContentText(normalized.content).trim(),
|
|
404
|
+
...(normalized.options ? { options: normalized.options } : {}),
|
|
405
|
+
};
|
|
398
406
|
return carryLifecycleToken(marker ? { ...base, ...marker } : base, entry);
|
|
399
407
|
}
|
|
400
408
|
|
|
@@ -427,6 +435,20 @@ function normalizePersistedEntry(entry, options = {}) {
|
|
|
427
435
|
const message = entry.message.trim();
|
|
428
436
|
return message ? { id, message, enqueuedAt } : null;
|
|
429
437
|
}
|
|
438
|
+
if (Object.prototype.hasOwnProperty.call(entry, 'content')) {
|
|
439
|
+
const normalized = normalizePendingMessageEntry(entry);
|
|
440
|
+
if (!normalized) return null;
|
|
441
|
+
if (typeof normalized.content === 'string' && !normalized.options) {
|
|
442
|
+
return { id, message: normalized.text || normalized.content, enqueuedAt };
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
id,
|
|
446
|
+
content: normalized.content,
|
|
447
|
+
text: normalized.text,
|
|
448
|
+
...(normalized.options ? { options: normalized.options } : {}),
|
|
449
|
+
enqueuedAt,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
430
452
|
const t = pendingMessageText(entry);
|
|
431
453
|
return t ? { id, message: t, enqueuedAt } : null;
|
|
432
454
|
}
|
|
@@ -1195,10 +1217,26 @@ async function _flushForeignDrainBatch() {
|
|
|
1195
1217
|
&& !isCompletionNotificationEntry(entry)
|
|
1196
1218
|
&& !isLegacyUnmarkedCompletionNotification(text)
|
|
1197
1219
|
&& text && !isInternalRuntimeNotificationText(text);
|
|
1220
|
+
const normalized = normalizePendingMessageEntry(entry);
|
|
1221
|
+
const structured = Array.isArray(normalized?.content) || Boolean(normalized?.options);
|
|
1198
1222
|
if (foreignUser && isStaleUserInjection(entry)) {
|
|
1199
|
-
|
|
1223
|
+
const lateText = lateDeliveryText(text, entry);
|
|
1224
|
+
const content = Array.isArray(normalized?.content)
|
|
1225
|
+
? [{ type: 'text', text: `${lateText.slice(0, lateText.length - text.length)}` }, ...normalized.content]
|
|
1226
|
+
: lateText;
|
|
1227
|
+
taken.push({
|
|
1228
|
+
...(structured ? { content } : {}),
|
|
1229
|
+
text: lateText,
|
|
1230
|
+
id,
|
|
1231
|
+
...(normalized?.options ? { options: normalized.options } : {}),
|
|
1232
|
+
});
|
|
1200
1233
|
} else if (foreignUser) {
|
|
1201
|
-
taken.push({
|
|
1234
|
+
taken.push({
|
|
1235
|
+
...(structured ? { content: normalized?.content ?? text } : {}),
|
|
1236
|
+
text,
|
|
1237
|
+
id,
|
|
1238
|
+
...(normalized?.options ? { options: normalized.options } : {}),
|
|
1239
|
+
});
|
|
1202
1240
|
} else {
|
|
1203
1241
|
kept.push(entry);
|
|
1204
1242
|
}
|
|
@@ -6,8 +6,10 @@ import { executeInternalTool } from '../../internal-tools.mjs';
|
|
|
6
6
|
import { classifyResultKind } from '../result-classification.mjs';
|
|
7
7
|
import { tryPrefetchCached, setPrefetchCached } from '../read-dedup.mjs';
|
|
8
8
|
import { _executeCodeGraphToolLazy } from './runtime-loaders.mjs';
|
|
9
|
+
import { runAbortable, throwIfAborted } from '../../../../shared/abort-race.mjs';
|
|
9
10
|
|
|
10
|
-
export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
11
|
+
export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch, signal = null) {
|
|
12
|
+
throwIfAborted(signal);
|
|
11
13
|
if (!explicitPrefetch || typeof explicitPrefetch !== 'object') return null;
|
|
12
14
|
if (!isAgentOwner(session)) return null;
|
|
13
15
|
const parts = [];
|
|
@@ -52,7 +54,10 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
52
54
|
totalEntries.push(...files);
|
|
53
55
|
// R20: per-file prefetch cache (cross-dispatch, process-local).
|
|
54
56
|
// Try each file from cache first; batch misses into one disk read.
|
|
55
|
-
const { resolve: _pfResolve, isAbsolute: _pfIsAbs, normalize: _pfNorm } = await
|
|
57
|
+
const { resolve: _pfResolve, isAbsolute: _pfIsAbs, normalize: _pfNorm } = await runAbortable(
|
|
58
|
+
signal,
|
|
59
|
+
() => import('path'),
|
|
60
|
+
);
|
|
56
61
|
const _pfCwd = session.cwd || null;
|
|
57
62
|
function _pfAbsPath(f) {
|
|
58
63
|
const abs = _pfIsAbs(f) ? f : _pfResolve(_pfCwd || process.cwd(), f);
|
|
@@ -79,7 +84,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
79
84
|
if (missFiles.length > 0) {
|
|
80
85
|
// Read each miss file individually so we can cache per-file.
|
|
81
86
|
// The files list is small (typically 2-5), so N awaits is fine.
|
|
82
|
-
await Promise.all(missFiles.map(async (f) => {
|
|
87
|
+
await runAbortable(signal, () => Promise.all(missFiles.map(async (f) => {
|
|
83
88
|
const opts = _readOptsByFile.get(f) || {};
|
|
84
89
|
const readArgs = { path: f };
|
|
85
90
|
if (opts.mode === 'full') {
|
|
@@ -95,7 +100,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
95
100
|
if (out !== null) {
|
|
96
101
|
missResults[f] = String(out);
|
|
97
102
|
}
|
|
98
|
-
}));
|
|
103
|
+
})));
|
|
99
104
|
// Cache successful miss results.
|
|
100
105
|
for (const { file, abs } of fileMisses) {
|
|
101
106
|
const content = missResults[file];
|
|
@@ -157,7 +162,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
157
162
|
return { symbol, out: null };
|
|
158
163
|
});
|
|
159
164
|
});
|
|
160
|
-
const callerResults = await Promise.allSettled(callerTasks);
|
|
165
|
+
const callerResults = await runAbortable(signal, () => Promise.allSettled(callerTasks));
|
|
161
166
|
for (const r of callerResults) {
|
|
162
167
|
const { symbol, out } = r.status === 'fulfilled' ? r.value : { symbol: '?', out: null };
|
|
163
168
|
if (out && classifyResultKind(String(out)) !== 'error') {
|
|
@@ -182,7 +187,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
182
187
|
return { symbol, out: null };
|
|
183
188
|
});
|
|
184
189
|
});
|
|
185
|
-
const refResults = await Promise.allSettled(refTasks);
|
|
190
|
+
const refResults = await runAbortable(signal, () => Promise.allSettled(refTasks));
|
|
186
191
|
for (const r of refResults) {
|
|
187
192
|
const { symbol, out } = r.status === 'fulfilled' ? r.value : { symbol: '?', out: null };
|
|
188
193
|
if (out && classifyResultKind(String(out)) !== 'error') {
|
|
@@ -192,6 +197,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
}
|
|
200
|
+
throwIfAborted(signal);
|
|
195
201
|
if (session && typeof session === 'object' && (callers.length > 0 || references.length > 0)) {
|
|
196
202
|
if (!session.prefetchStats) session.prefetchStats = { files: 0, cached: 0, miss: 0, failed: 0, callers: 0, references: 0 };
|
|
197
203
|
session.prefetchStats.callers = (session.prefetchStats.callers || 0) + callers.length;
|
|
@@ -4,13 +4,14 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { isInternalRuntimeNotificationText as contractIsInternalRuntimeNotificationText } from '../../../../shared/tool-execution-contract.mjs';
|
|
6
6
|
import { SUMMARY_PREFIX } from '../compact.mjs';
|
|
7
|
+
import { attachmentTextForPart, isAttachmentReference } from '../../../../attachments/store.mjs';
|
|
7
8
|
|
|
8
9
|
export function promptContentText(content) {
|
|
9
10
|
if (typeof content === 'string') return content;
|
|
10
11
|
if (Array.isArray(content)) {
|
|
11
12
|
return content.map((part) => {
|
|
12
13
|
if (typeof part === 'string') return part;
|
|
13
|
-
if (part?.type === 'text') return part.text || '';
|
|
14
|
+
if (part?.type === 'text') return isAttachmentReference(part) ? attachmentTextForPart(part) : (part.text || '');
|
|
14
15
|
if (part?.type === 'image') return '[Image]';
|
|
15
16
|
return part?.text || '';
|
|
16
17
|
}).filter(Boolean).join('\n');
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// Per-session mutex extracted verbatim from manager.mjs. Queues concurrent
|
|
3
3
|
// askSession calls (and their drained pending-message tail turns) to prevent
|
|
4
4
|
// message loss / interleaving.
|
|
5
|
+
import { abortReason } from '../../../../shared/abort-race.mjs';
|
|
6
|
+
|
|
5
7
|
const _sessionLocks = new Map();
|
|
6
|
-
export function acquireSessionLock(sessionId) {
|
|
8
|
+
export function acquireSessionLock(sessionId, signal = null) {
|
|
7
9
|
let entry = _sessionLocks.get(sessionId);
|
|
8
10
|
if (!entry) {
|
|
9
11
|
entry = { promise: Promise.resolve(), count: 0 };
|
|
@@ -15,9 +17,43 @@ export function acquireSessionLock(sessionId) {
|
|
|
15
17
|
entry.promise = new Promise(r => { release = r; });
|
|
16
18
|
// Self-heal: if the previous holder rejected, swallow so subsequent
|
|
17
19
|
// queued waiters don't propagate that rejection and brick the lock chain.
|
|
18
|
-
|
|
20
|
+
const previousReady = prev.catch(() => {});
|
|
21
|
+
let slotReleased = false;
|
|
22
|
+
const releaseSlot = () => {
|
|
23
|
+
if (slotReleased) return;
|
|
24
|
+
slotReleased = true;
|
|
19
25
|
entry.count--;
|
|
20
26
|
if (entry.count === 0) _sessionLocks.delete(sessionId);
|
|
21
27
|
release();
|
|
28
|
+
};
|
|
29
|
+
const unlock = () => releaseSlot();
|
|
30
|
+
if (!signal) return previousReady.then(() => unlock);
|
|
31
|
+
|
|
32
|
+
// A cancelled waiter must reject immediately without opening a hole in the
|
|
33
|
+
// mutex. Its reserved slot is skipped only after the previous holder exits,
|
|
34
|
+
// so later waiters still cannot overlap the live owner.
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let settled = false;
|
|
37
|
+
const removeAbort = () => {
|
|
38
|
+
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
39
|
+
};
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
if (settled) return;
|
|
42
|
+
settled = true;
|
|
43
|
+
removeAbort();
|
|
44
|
+
void previousReady.then(releaseSlot);
|
|
45
|
+
reject(abortReason(signal, `Session "${sessionId}" lock wait aborted`));
|
|
46
|
+
};
|
|
47
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
onAbort();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
previousReady.then(() => {
|
|
53
|
+
if (settled) return;
|
|
54
|
+
settled = true;
|
|
55
|
+
removeAbort();
|
|
56
|
+
resolve(unlock);
|
|
57
|
+
});
|
|
22
58
|
});
|
|
23
59
|
}
|
|
@@ -4,12 +4,9 @@ import { renameWithRetrySync } from '../../../../shared/atomic-file.mjs';
|
|
|
4
4
|
import { sanitizeContentForStoredHistory } from '../../providers/media-normalization.mjs';
|
|
5
5
|
import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from '../lifecycle-scan.mjs';
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// text placeholder at serialization time — keeping session files small without
|
|
11
|
-
// starving the model of the image mid-conversation. Returns the same object
|
|
12
|
-
// reference when nothing changed (no-image sessions pay only a shallow scan).
|
|
7
|
+
// Inline legacy media is replaced with placeholders on disk. New prompt media
|
|
8
|
+
// already arrives as durable content-addressed refs, so session JSON stays small
|
|
9
|
+
// while provider lowering can still resolve it across turns.
|
|
13
10
|
// Per-message disk projection shared by _sessionForDisk and the save-worker
|
|
14
11
|
// delta path (tail-only projection). Returns the SAME array reference when no
|
|
15
12
|
// message changed. Idempotent: already-sanitized content passes through
|
|
@@ -121,7 +121,11 @@ export function _queueSessionSummaryUpsert(session, version = null) {
|
|
|
121
121
|
*/
|
|
122
122
|
export function _queueSessionSummaryUpsertRow(row, version = null) {
|
|
123
123
|
if (!row) return;
|
|
124
|
-
|
|
124
|
+
const fileProbe = probePath(join(getStoreDir(), `${row.id}.json`));
|
|
125
|
+
const durableRow = fileProbe.state === PROBE_PRESENT
|
|
126
|
+
? { ...row, storageMtimeMs: fileProbe.mtimeMs, storageSize: fileProbe.size }
|
|
127
|
+
: row;
|
|
128
|
+
_setCachedBaseSummary(durableRow);
|
|
125
129
|
if (version === null || (_summaryCacheVersions.get(row.id) || 0) === version) {
|
|
126
130
|
_summaryCacheUpserts.delete(row.id);
|
|
127
131
|
if (_summaryCacheLatestRows.get(row.id)?.version === version) {
|
|
@@ -130,7 +134,7 @@ export function _queueSessionSummaryUpsertRow(row, version = null) {
|
|
|
130
134
|
_summaryCacheRemovals.delete(row.id);
|
|
131
135
|
_invalidateSummaryRowsView();
|
|
132
136
|
}
|
|
133
|
-
_upsertSessionSummaryRow(
|
|
137
|
+
_upsertSessionSummaryRow(durableRow);
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
export function _queueSessionSummaryRemoval(id) {
|
|
@@ -220,7 +224,10 @@ export function _scanStoredSessionSummaryRows() {
|
|
|
220
224
|
else markInvalid(f);
|
|
221
225
|
continue;
|
|
222
226
|
}
|
|
223
|
-
const
|
|
227
|
+
const summary = session ? _sessionSummary(session) : null;
|
|
228
|
+
const row = summary
|
|
229
|
+
? { ...summary, storageMtimeMs: fileProbe.mtimeMs, storageSize: fileProbe.size }
|
|
230
|
+
: null;
|
|
224
231
|
_summaryScanCache.set(f, { mtimeMs: fileProbe.mtimeMs, size: fileProbe.size, row });
|
|
225
232
|
changed = true;
|
|
226
233
|
if (row) rows.push(row);
|
|
@@ -154,6 +154,8 @@ export function _sessionSummary(session) {
|
|
|
154
154
|
preview: messageProjection.preview,
|
|
155
155
|
generation: typeof session.generation === 'number' ? session.generation : 0,
|
|
156
156
|
implicitBashSessionId: session.implicitBashSessionId || null,
|
|
157
|
+
storageMtimeMs: _positiveNumber(session.storageMtimeMs, 0),
|
|
158
|
+
storageSize: _positiveNumber(session.storageSize, 0),
|
|
157
159
|
// Lifecycle provenance for catalog filters: resume-machinery scratch
|
|
158
160
|
// forks (detachedReason='cli-resume') must never surface in Recent.
|
|
159
161
|
detachedReason: session.detachedReason || null,
|
|
@@ -190,6 +192,8 @@ function _normalizeSummaryRow(row) {
|
|
|
190
192
|
preview: _cleanPreview(row.preview || ''),
|
|
191
193
|
generation: typeof row.generation === 'number' ? row.generation : 0,
|
|
192
194
|
implicitBashSessionId: row.implicitBashSessionId || null,
|
|
195
|
+
storageMtimeMs: _positiveNumber(row.storageMtimeMs, 0),
|
|
196
|
+
storageSize: _positiveNumber(row.storageSize, 0),
|
|
193
197
|
detachedReason: row.detachedReason || null,
|
|
194
198
|
};
|
|
195
199
|
}
|