mixdog 1.3.2 → 1.3.4
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 +8 -8
- package/scripts/verify-runtime-dependency-versions.mjs +158 -0
- package/src/agents/reviewer/AGENT.md +5 -7
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -3
- package/src/defaults/skills/local-provider/SKILL.md +33 -6
- package/src/rules/lead/LEAD.md +5 -8
- package/src/rules/shared/45-destructive.md +5 -1
- package/src/rules/shared/60-verification.md +6 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
- package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
- package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
- package/src/runtime/attachments/legacy-office.mjs +110 -0
- package/src/runtime/attachments/prompt-restore.mjs +70 -0
- package/src/runtime/attachments/store.mjs +60 -3
- package/src/runtime/effort-judge/judge-client.mjs +61 -14
- package/src/runtime/effort-judge/model-install.mjs +34 -2
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
- package/src/runtime/office/portable/portable-soffice.mjs +132 -30
- package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
- package/src/runtime/shared/agent-avatar.mjs +3 -2
- package/src/runtime/shared/agent-room-host.d.mts +31 -0
- package/src/runtime/shared/agent-room-host.mjs +58 -0
- package/src/runtime/shared/bot-hops.mjs +16 -11
- package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
- package/src/runtime/shared/redact-directories.mjs +18 -0
- package/src/runtime/shared/remove-directory.mjs +28 -0
- package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
- package/src/runtime/shared/session-sidebar-host.mjs +16 -3
- package/src/runtime/shared/tool-execution-contract.mjs +4 -0
- package/src/runtime/shared/tool-work-units.mjs +12 -9
- package/src/runtime/shared/turn-snapshot.mjs +233 -122
- package/src/runtime/shared/user-data-guard.mjs +51 -19
- package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
- package/src/runtime/shared/worktrees/contract.mjs +11 -3
- package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
- package/src/runtime/shared/worktrees/manager.mjs +419 -52
- package/src/runtime/shared/worktrees/registry.mjs +3 -1
- package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
- package/src/runtime/tidy/process.mjs +41 -8
- package/src/runtime/tidy/resolve.mjs +10 -7
- package/src/session-runtime/agent-home-delivery.mjs +73 -91
- package/src/session-runtime/agent-profile-changes.mjs +16 -0
- package/src/session-runtime/agent-room-delivery.mjs +2 -63
- package/src/session-runtime/agent-task-sessions.mjs +194 -64
- package/src/session-runtime/agent-tool-routing.mjs +49 -4
- package/src/session-runtime/boot/apis.mjs +3 -0
- package/src/session-runtime/boot/facade.mjs +5 -6
- package/src/session-runtime/boot/tools.mjs +23 -1
- package/src/session-runtime/held-requests.mjs +20 -25
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
- package/src/session-runtime/model-recency.mjs +1 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
- package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
- package/src/session-runtime/runtime-review-api.mjs +4 -1
- package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
- package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +44 -20
- package/src/session-runtime/services/agent-tool/render.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +12 -2
- package/src/session-runtime/services/worktree-access.mjs +27 -0
- package/src/session-runtime/session-hooks.mjs +9 -17
- package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
- package/src/session-runtime/setup-tool/executor.mjs +4 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
- package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
- package/src/session-runtime/tool-policy-refresh.mjs +1 -0
- package/src/session-runtime/tool-surface.mjs +9 -5
- package/src/session-runtime/turn/session-ops.mjs +8 -0
- package/src/session-runtime/turn/turn-run.mjs +5 -1
- package/src/session-runtime/usage-stats-api.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
- package/src/session-runtime/workspace/transition-steps.mjs +1 -18
- package/src/session-runtime/workspace/transitions.mjs +279 -67
- package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
- package/src/standalone/daemon-lag-profiler.mjs +3 -1
- package/src/standalone/daemon-session-control.mjs +140 -23
- package/src/standalone/daemon-telemetry.mjs +9 -0
- package/src/standalone/daemon.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +11 -2
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
- package/src/standalone/session-service/agent-route-follow.mjs +69 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
- package/src/standalone/session-service.mjs +10 -0
- package/src/standalone/session-tool-output.mjs +1 -0
- package/src/standalone/staged-import.mjs +47 -0
- package/src/tui/App.jsx +2 -0
- package/src/tui/app/message-selector.mjs +15 -2
- package/src/tui/app/model-options.mjs +16 -2
- package/src/tui/app/model-picker.mjs +2 -1
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/dist/index.mjs +403 -210
- package/src/tui/paste-attachments.mjs +0 -1
- package/src/tui/paste-text-policy.mjs +4 -5
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
- package/src/tui/session/notice-surface.mjs +3 -0
- package/src/tui/session/session-api/intake/abort.mjs +33 -17
- package/src/tui/session/session-api/intake/rewind.mjs +39 -4
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +11 -48
- package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
- package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
- package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
- package/src/session-runtime/room-member-gate.mjs +0 -61
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
|
@@ -13,6 +13,7 @@ import { _queueSessionSummaryUpsert, _queueSessionSummaryRemoval } from '../summ
|
|
|
13
13
|
import { deleteSession } from '../session-delete.mjs';
|
|
14
14
|
import { markSessionClosed } from '../lifecycle-barriers.mjs';
|
|
15
15
|
import { BLANK_SCRATCH_MAX_AGE_MS, RUNNING_STALL_MS, SWEEP_RECORD_NO_DATA, readSweepRecord } from './sweep-record.mjs';
|
|
16
|
+
import { subAgentMember } from './sub-agent-retention.mjs';
|
|
16
17
|
import { isAgentHomeSessionId } from '../../agent-home.mjs';
|
|
17
18
|
import { isRoomThreadSessionId } from '../../agent-room.mjs';
|
|
18
19
|
|
|
@@ -28,6 +29,14 @@ const effectiveFields = (actual, row) => ({
|
|
|
28
29
|
createdAt: Number(actual?.createdAt) > 0 ? Number(actual.createdAt) : row.createdAt || 0,
|
|
29
30
|
});
|
|
30
31
|
|
|
32
|
+
/** Every sub-agent record, open or closed, fresh or not, for the tree
|
|
33
|
+
* retention its caller applies (sub-agent-retention.mjs). */
|
|
34
|
+
function noteSubAgent(ctx, id, doc) {
|
|
35
|
+
if (!ctx.plan.retainSubAgents) return;
|
|
36
|
+
const member = subAgentMember(id, doc);
|
|
37
|
+
if (member) ctx.tally.subAgents.push(member);
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
/** A closed record: a mature tombstone is deleted; otherwise a stale summary
|
|
32
41
|
* row that still claims the session open is repaired and the row is kept. */
|
|
33
42
|
function sweepClosedRecord(ctx, row, actual, jsonPath) {
|
|
@@ -153,6 +162,7 @@ function sweepOpenRecord(ctx, row, record, { jsonMtime }) {
|
|
|
153
162
|
tally.remaining++;
|
|
154
163
|
return;
|
|
155
164
|
}
|
|
165
|
+
noteSubAgent(ctx, row.id, record.doc);
|
|
156
166
|
const eff = effectiveFields(record.doc, row);
|
|
157
167
|
const ownerNamed = typeof eff.owner === 'string' && eff.owner.length > 0;
|
|
158
168
|
// Freshness gate — recently-touched open sessions are skipped cheaply here.
|
|
@@ -277,7 +287,10 @@ export function sweepRow(ctx, row) {
|
|
|
277
287
|
return;
|
|
278
288
|
}
|
|
279
289
|
if (record.closed === true || actual.status === 'closed') {
|
|
290
|
+
const cleanedBefore = ctx.tally.tombstonesCleaned;
|
|
280
291
|
sweepClosedRecord(ctx, row, actual, jsonPath);
|
|
292
|
+
// A tombstone this sweep just deleted is gone, not a member.
|
|
293
|
+
if (ctx.tally.tombstonesCleaned === cleanedBefore) noteSubAgent(ctx, row.id, actual);
|
|
281
294
|
return;
|
|
282
295
|
}
|
|
283
296
|
sweepOpenRecord(ctx, row, record, { jsonMtime: jsonProbe.mtimeMs || 0 });
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
13
13
|
import { stat } from 'node:fs/promises';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
+
import { performance } from 'node:perf_hooks';
|
|
15
16
|
// Leaf helpers only: the three-way present/absent/unreadable classification
|
|
16
17
|
// and the strict record parser the authoritative store uses, so the catalog
|
|
17
18
|
// cannot disagree with it.
|
|
@@ -384,6 +385,66 @@ function scanSessionEntries(directory, indexRowsById, forceRead) {
|
|
|
384
385
|
return { entries };
|
|
385
386
|
}
|
|
386
387
|
|
|
388
|
+
// The async rescan yields a turn once a slice of reconciles has run this long:
|
|
389
|
+
// a full authoritative rescan strict-reads every session file (≈4 s for 1.8k
|
|
390
|
+
// files / 1.3 GB), which must not hold the daemon loop in one piece.
|
|
391
|
+
let scanSliceMs = 25;
|
|
392
|
+
|
|
393
|
+
/** Test seam: the async rescan's slice budget (0 yields after every file). */
|
|
394
|
+
export function _setCatalogScanSliceMsForTest(ms) {
|
|
395
|
+
scanSliceMs = Math.max(0, Number(ms) || 0);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const nextTurn = () => new Promise((resolve) => setImmediate(resolve));
|
|
399
|
+
|
|
400
|
+
/** The entry a file still has: `entry` when the file's stamp is unchanged
|
|
401
|
+
* since it was reconciled, else a fresh reconcile (null once it is gone). */
|
|
402
|
+
function settledEntry(directory, storageId, entry, retained, forceRead) {
|
|
403
|
+
if (entry?.stamp) {
|
|
404
|
+
let stamp = null;
|
|
405
|
+
try {
|
|
406
|
+
stamp = statSessionStamp(join(directory, `${storageId}.json`));
|
|
407
|
+
} catch {
|
|
408
|
+
// Gone or unstatable: the reconcile below decides which.
|
|
409
|
+
}
|
|
410
|
+
if (stamp && sameSessionStamp(entry.stamp, stamp)) return entry;
|
|
411
|
+
}
|
|
412
|
+
return reconcileSessionFile(directory, storageId, retained, forceRead);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** scanSessionEntries in slices, yielding the loop between them. Files can
|
|
416
|
+
* change while it yields, so the answer is settled in one synchronous pass at
|
|
417
|
+
* the end: the directory is listed again, a file whose stamp moved since its
|
|
418
|
+
* read (or that appeared) is reconciled again, and a file that is no longer
|
|
419
|
+
* listed is dropped — the entries match storage when they are returned, as
|
|
420
|
+
* the one-piece scan's do. That pass is a stat per file (≈20 ms for 1.5k). */
|
|
421
|
+
async function scanSessionEntriesAsync(directory, indexRowsById, forceRead, onSettle) {
|
|
422
|
+
const sessionIds = listSessionIds(directory);
|
|
423
|
+
if (!sessionIds) {
|
|
424
|
+
onSettle?.();
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
const scanned = new Map();
|
|
428
|
+
let sliceStart = performance.now();
|
|
429
|
+
for (const storageId of sessionIds) {
|
|
430
|
+
if (performance.now() - sliceStart >= scanSliceMs) {
|
|
431
|
+
await nextTurn();
|
|
432
|
+
sliceStart = performance.now();
|
|
433
|
+
}
|
|
434
|
+
const entry = reconcileSessionFile(directory, storageId, indexRowsById.get(storageId), forceRead);
|
|
435
|
+
if (entry) scanned.set(storageId, entry);
|
|
436
|
+
}
|
|
437
|
+
onSettle?.();
|
|
438
|
+
const settledIds = listSessionIds(directory);
|
|
439
|
+
if (!settledIds) return null;
|
|
440
|
+
const entries = new Map();
|
|
441
|
+
for (const storageId of settledIds) {
|
|
442
|
+
const entry = settledEntry(directory, storageId, scanned.get(storageId), indexRowsById.get(storageId), forceRead);
|
|
443
|
+
if (entry) entries.set(storageId, entry);
|
|
444
|
+
}
|
|
445
|
+
return { entries };
|
|
446
|
+
}
|
|
447
|
+
|
|
387
448
|
/** The row an entry contributes, or null. */
|
|
388
449
|
function entryRow(entry, id, indexRowsById) {
|
|
389
450
|
if (!entry || entry.kind === 'none') return null;
|
|
@@ -419,6 +480,21 @@ export function scanSessionFiles(indexRowsById, forceRead) {
|
|
|
419
480
|
return scan && assembleCatalog(scan.entries, indexRowsById);
|
|
420
481
|
}
|
|
421
482
|
|
|
483
|
+
/** scanSessionFiles with the same answer, its file reconciles run in slices
|
|
484
|
+
* that yield the event loop (the daemon's session.list storage refresh).
|
|
485
|
+
* `onSettle` runs once, when the answer's final synchronous part begins: from
|
|
486
|
+
* then on storage changes are no longer seen by this scan. */
|
|
487
|
+
export async function scanSessionFilesAsync(indexRowsById, forceRead, { onSettle } = {}) {
|
|
488
|
+
const directory = join(dataDir(), 'sessions');
|
|
489
|
+
const dirProbe = probePath(directory);
|
|
490
|
+
if (dirProbe.state !== PROBE_PRESENT) {
|
|
491
|
+
onSettle?.();
|
|
492
|
+
return dirProbe.state === PROBE_ABSENT ? [] : null;
|
|
493
|
+
}
|
|
494
|
+
const scan = await scanSessionEntriesAsync(directory, indexRowsById, forceRead, onSettle);
|
|
495
|
+
return scan && assembleCatalog(scan.entries, indexRowsById);
|
|
496
|
+
}
|
|
497
|
+
|
|
422
498
|
/** Listing from the index rows alone, or null without a valid index. */
|
|
423
499
|
export function indexListing(index) {
|
|
424
500
|
return index.rows && leadRows(index.rows.map((row) => ({ ...row }))).sort(byCatalogOrder);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
indexListing,
|
|
21
21
|
observeStamp,
|
|
22
22
|
scanSessionFiles,
|
|
23
|
+
scanSessionFilesAsync,
|
|
23
24
|
stampedDerivation,
|
|
24
25
|
} from './store-summary-catalog.mjs';
|
|
25
26
|
import { createStoredTranscriptCache, nextProjectionStamp } from './store-transcript-cache.mjs';
|
|
@@ -155,6 +156,30 @@ async function catalogDbListing(page) {
|
|
|
155
156
|
return answer;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
// The running storage refresh a new caller may still join: only until its
|
|
160
|
+
// final synchronous settle begins, since a caller arriving after that asked
|
|
161
|
+
// after the answer was fixed (a scan that never yields settles before its own
|
|
162
|
+
// caller awaits it).
|
|
163
|
+
let storageRefreshFlight = null;
|
|
164
|
+
|
|
165
|
+
async function refreshFromStorageOnce(onSettle) {
|
|
166
|
+
const index = ordinaryCatalog.loadIndex();
|
|
167
|
+
return (await scanSessionFilesAsync(index.rowsById, true, { onSettle })) ?? indexListing(index) ?? [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function storageRefresh() {
|
|
171
|
+
if (storageRefreshFlight?.joinable) return storageRefreshFlight.rows;
|
|
172
|
+
const flight = { joinable: true, rows: null };
|
|
173
|
+
storageRefreshFlight = flight;
|
|
174
|
+
flight.rows = refreshFromStorageOnce(() => {
|
|
175
|
+
flight.joinable = false;
|
|
176
|
+
}).finally(() => {
|
|
177
|
+
// A newer flight is never cleared by an older one's end.
|
|
178
|
+
if (storageRefreshFlight === flight) storageRefreshFlight = null;
|
|
179
|
+
});
|
|
180
|
+
return flight.rows;
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
/**
|
|
159
184
|
* listStoredSessionSummaries for async callers (the daemon's session views).
|
|
160
185
|
* In the daemon the ordinary listing comes from the session catalog DB
|
|
@@ -163,7 +188,14 @@ async function catalogDbListing(page) {
|
|
|
163
188
|
* JSON/file path.
|
|
164
189
|
*/
|
|
165
190
|
export async function listStoredSessionSummariesAsync(options = {}) {
|
|
166
|
-
if (options.refreshFromStorage === true
|
|
191
|
+
if (options.refreshFromStorage === true) {
|
|
192
|
+
// The same authoritative rescan as the sync path, in slices that yield.
|
|
193
|
+
// Concurrent refreshes share one scan while it can still see their
|
|
194
|
+
// request (it settles against storage when it ends); each caller gets its
|
|
195
|
+
// own rows.
|
|
196
|
+
return (await storageRefresh()).map((row) => ({ ...row }));
|
|
197
|
+
}
|
|
198
|
+
if (options.rebuildIfMissing === false) {
|
|
167
199
|
return listStoredSessionSummaries(options);
|
|
168
200
|
}
|
|
169
201
|
const answer = await catalogDbListing(null);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function isRootLeadSession(value: unknown): boolean;
|
|
2
|
+
export function sessionVisibility(value: unknown): 'ordinary' | 'agent-only';
|
|
3
|
+
export function isAgentOnlySession(value: unknown): boolean;
|
|
4
|
+
export function agentParentIds(value: unknown): string[];
|
|
5
|
+
export function agentDescendantIds(rows: readonly unknown[], rootId: unknown): string[];
|
|
@@ -49,3 +49,40 @@ export function sessionVisibility(value) {
|
|
|
49
49
|
export function isAgentOnlySession(value) {
|
|
50
50
|
return sessionVisibility(value) === SESSION_VISIBILITY_AGENT_ONLY;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/** Every session a record links itself to as its owner or parent (both links,
|
|
54
|
+
* never itself): the links the descendant walk and the sub-agent retention
|
|
55
|
+
* trees follow. */
|
|
56
|
+
export function agentParentIds(value) {
|
|
57
|
+
const id = clean(value?.id);
|
|
58
|
+
return [...new Set([clean(value?.ownerSessionId), clean(value?.parentSessionId)])].filter(
|
|
59
|
+
(parent) => parent && parent !== id
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The agent-only sessions under `rootId` at any depth, from session rows. A
|
|
64
|
+
* catalog row links a sub-agent to its root Lead (`ownerSessionId`) or, on a
|
|
65
|
+
* legacy row, to its parent, so the walk follows every link a row names. */
|
|
66
|
+
export function agentDescendantIds(rows, rootId) {
|
|
67
|
+
const root = clean(rootId);
|
|
68
|
+
const children = new Map();
|
|
69
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
70
|
+
if (!isAgentOnlySession(row)) continue;
|
|
71
|
+
const id = clean(row.id);
|
|
72
|
+
if (!id) continue;
|
|
73
|
+
for (const parent of agentParentIds(row)) {
|
|
74
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
75
|
+
children.get(parent).push(id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const found = new Set();
|
|
79
|
+
const queue = [root];
|
|
80
|
+
while (queue.length) {
|
|
81
|
+
for (const child of children.get(queue.pop()) || []) {
|
|
82
|
+
if (child === root || found.has(child)) continue;
|
|
83
|
+
found.add(child);
|
|
84
|
+
queue.push(child);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...found];
|
|
88
|
+
}
|
|
@@ -722,4 +722,5 @@ export { fenceSessionWrites } from './store/live-state.mjs';
|
|
|
722
722
|
// session-store entry point.
|
|
723
723
|
export { listStoredSessions, listStoredSessionSummaries, getStoredSessionsRaw } from './store/listing.mjs';
|
|
724
724
|
export { sweepStaleSessions, sweepStaleSessionsCooperative } from './store/sweep/stale-sweep.mjs';
|
|
725
|
+
export { applySubAgentRetention } from './store/sweep/sub-agent-retention.mjs';
|
|
725
726
|
export { _savePending } from './store/pending-saves.mjs';
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// clean up after crashed holders. Pair with the in-process withPathLock for
|
|
4
4
|
// the same target: in-process serialises async callers in this Node, advisory
|
|
5
5
|
// lock serialises across Node processes.
|
|
6
|
-
import { openSync, closeSync, writeSync, readFileSync, unlinkSync,
|
|
6
|
+
import { openSync, closeSync, writeSync, readFileSync, unlinkSync, realpathSync } from 'node:fs';
|
|
7
7
|
import { compareCodePoints } from '../../../../shared/code-point-order.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { ensurePrivateRuntimeRoot } from '../../../../shared/runtime-root.mjs';
|
|
9
9
|
import { sleep } from '../../../../shared/sleep.mjs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
10
11
|
import { dirname, basename, join, resolve } from 'node:path';
|
|
11
12
|
import { createHash, randomBytes } from 'node:crypto';
|
|
12
13
|
|
|
@@ -28,14 +29,24 @@ function canonicalLockTarget(targetPath) {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
32
|
+
// External files can be shared by profiles, so neither MIXDOG_HOME/DATA_DIR
|
|
33
|
+
// nor MIXDOG_RUNTIME_ROOT may choose this namespace. Temp directories can
|
|
34
|
+
// differ between processes too. Keep locks in the OS user's local home,
|
|
35
|
+
// never beside the target: sync drives such as MYBOX refuse sibling dotfiles.
|
|
36
|
+
// The pid liveness check coordinates only processes on this host.
|
|
37
|
+
function lockDirectory() {
|
|
38
|
+
const root =
|
|
39
|
+
process.platform === 'win32'
|
|
40
|
+
? join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'mixdog')
|
|
41
|
+
: join(homedir(), '.mixdog');
|
|
42
|
+
return join(root, 'locks');
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
function lockFileFor(targetPath) {
|
|
35
46
|
const canonical = canonicalLockTarget(targetPath);
|
|
36
47
|
const key = process.platform === 'win32' ? canonical.toLowerCase() : canonical;
|
|
37
48
|
const hash = createHash('sha256').update(key).digest('hex');
|
|
38
|
-
return join(
|
|
49
|
+
return join(lockDirectory(), `${hash}.mixdog-lock`);
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
function isPidAlive(pid) {
|
|
@@ -112,7 +123,7 @@ function tryCreateLock(lockFile, token) {
|
|
|
112
123
|
|
|
113
124
|
async function acquireAdvisoryLock(targetPath, { timeoutMs = 5000, pollMs = 25 } = {}) {
|
|
114
125
|
const lockFile = lockFileFor(targetPath);
|
|
115
|
-
|
|
126
|
+
ensurePrivateRuntimeRoot(dirname(lockFile));
|
|
116
127
|
const token = `${process.pid}.${randomBytes(8).toString('hex')}`;
|
|
117
128
|
const deadline = Date.now() + timeoutMs;
|
|
118
129
|
let stalePostDeadlineRetryUsed = false;
|
|
@@ -19,6 +19,7 @@ export function shellTaskIdentity(command, options) {
|
|
|
19
19
|
callerSessionId: options?.callerSessionId || options?.sessionId || null,
|
|
20
20
|
routingSessionId: options?.routingSessionId || options?.sessionId || null,
|
|
21
21
|
clientHostPid: options?.clientHostPid,
|
|
22
|
+
hop: options?.hop,
|
|
22
23
|
},
|
|
23
24
|
};
|
|
24
25
|
}
|
|
@@ -61,6 +62,7 @@ export function renderBackgroundedResult({ result, command, cwd, options, starte
|
|
|
61
62
|
callerSessionId: options?.callerSessionId || options?.sessionId,
|
|
62
63
|
routingSessionId: options?.routingSessionId || options?.sessionId,
|
|
63
64
|
clientHostPid: options?.clientHostPid,
|
|
65
|
+
hop: options?.hop,
|
|
64
66
|
});
|
|
65
67
|
} catch {
|
|
66
68
|
/* best effort */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUtf8 } from 'node:buffer';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
@@ -8,6 +9,10 @@ import { withBuiltinPathLocks } from './path-locks.mjs';
|
|
|
8
9
|
import { withAdvisoryLocks } from './advisory-lock.mjs';
|
|
9
10
|
import { gitRepoLockKey, withGitWorktreeWriteLock } from './git-repo-rw-lock.mjs';
|
|
10
11
|
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
12
|
+
import { tryAdmitWorkspaceActivity } from '../../../../shared/worktrees/activity-gate.mjs';
|
|
13
|
+
import { WORKTREE_SNAPSHOT_KIND } from '../../../../shared/worktrees/contract.mjs';
|
|
14
|
+
import { isWithin } from '../../../../shared/worktrees/paths.mjs';
|
|
15
|
+
import { readRegistrySync } from '../../../../shared/worktrees/registry.mjs';
|
|
11
16
|
import { runAbortable } from '../../../../shared/abort-race.mjs';
|
|
12
17
|
import { sleep } from '../../../../shared/sleep.mjs';
|
|
13
18
|
import { capMarker } from '../../../../shared/tool-output-marker.mjs';
|
|
@@ -26,6 +31,12 @@ import {
|
|
|
26
31
|
diffSnapshotMatches,
|
|
27
32
|
getDiffSnapshot,
|
|
28
33
|
} from './git-partial-stage.mjs';
|
|
34
|
+
import {
|
|
35
|
+
executeRawWorktree,
|
|
36
|
+
executeWorktreeAction,
|
|
37
|
+
WORKTREE_INPUT_FIELDS,
|
|
38
|
+
WORKTREE_TOOL_ACTIONS,
|
|
39
|
+
} from './git-worktree-tool.mjs';
|
|
29
40
|
|
|
30
41
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
31
42
|
const MAX_CAPTURE_BYTES = 128 * 1024 * 1024;
|
|
@@ -84,14 +95,15 @@ export const GIT_TOOL_DEF = {
|
|
|
84
95
|
compressible: true,
|
|
85
96
|
},
|
|
86
97
|
description:
|
|
87
|
-
'Run Git here, never through shell. An array (max 10) runs in order and stops on failure; batch read-only commands in one. diff for known changes, status to discover them; history only when needed. git diff or git diff -- <paths> includes untracked files. Set include_stage_ids:true to select changes, then action:stage with diff_id/change_ids. Staging rejects stale or cross-Project diffs and returns per-file staged counts.',
|
|
98
|
+
'Run Git here, never through shell. An array (max 10) runs in order and stops on failure; batch read-only commands in one. diff for known changes, status to discover them; history only when needed. git diff or git diff -- <paths> includes untracked files. Set include_stage_ids:true to select changes, then action:stage with diff_id/change_ids. Staging rejects stale or cross-Project diffs and returns per-file staged counts. Worktrees are managed only: action:worktree (Lead only) moves a session into/out of its managed worktree, merges, removes, lists, prunes; git worktree add/remove/list/prune map to managed operations (add -b keeps the session in place; add --detach makes a read-only snapshot worktree, removed when its turn ends; list --porcelain stays raw git; move/lock/unlock/repair are refused). Merge or remove only when the user asks; carry and large-size answers come from the user; force only on explicit request.',
|
|
88
99
|
inputSchema: {
|
|
89
100
|
type: 'object',
|
|
90
101
|
properties: {
|
|
91
102
|
action: {
|
|
92
103
|
type: 'string',
|
|
93
|
-
enum: ['command', 'stage'],
|
|
94
|
-
description:
|
|
104
|
+
enum: ['command', 'stage', 'worktree'],
|
|
105
|
+
description:
|
|
106
|
+
'Default command. stage uses diff_id/change_ids instead of command. worktree uses worktree_action and its fields.',
|
|
95
107
|
},
|
|
96
108
|
command: {
|
|
97
109
|
anyOf: [
|
|
@@ -118,6 +130,29 @@ export const GIT_TOOL_DEF = {
|
|
|
118
130
|
anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 50 }],
|
|
119
131
|
description: 'stage: exact change ID or IDs to stage, including new files.',
|
|
120
132
|
},
|
|
133
|
+
worktree_action: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
enum: [...WORKTREE_TOOL_ACTIONS],
|
|
136
|
+
description:
|
|
137
|
+
'enter: create or re-enter the managed worktree (branch: that branch, create: new); return: back to the project folder, worktree kept; merge: into its merge target; remove; list; prune.',
|
|
138
|
+
},
|
|
139
|
+
session_id: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'enter/return/merge/remove: another session (it must be idle); default the caller.',
|
|
142
|
+
},
|
|
143
|
+
name: { type: 'string', description: 'enter: the new worktree name (branch <prefix><name>).' },
|
|
144
|
+
branch: { type: 'string', description: 'enter: use the worktree of this branch instead.' },
|
|
145
|
+
create: { type: 'boolean', description: 'enter with branch: create that branch.' },
|
|
146
|
+
carry: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
enum: ['leave', 'carry'],
|
|
149
|
+
description: 'enter: the user answer to WORKTREE_DIRTY_CHOICE (uncommitted project-folder changes).',
|
|
150
|
+
},
|
|
151
|
+
confirm_large: {
|
|
152
|
+
type: 'boolean',
|
|
153
|
+
description: 'enter or git worktree add: the user accepted the WORKTREE_LARGE_REPO_CONFIRM size.',
|
|
154
|
+
},
|
|
155
|
+
force: { type: 'boolean', description: 'remove: discard dirty/unmerged work; only on explicit user request.' },
|
|
121
156
|
},
|
|
122
157
|
additionalProperties: false,
|
|
123
158
|
},
|
|
@@ -136,6 +171,9 @@ function ok(data = {}) {
|
|
|
136
171
|
});
|
|
137
172
|
}
|
|
138
173
|
|
|
174
|
+
const NOT_UTF8_STAGE_NOTE =
|
|
175
|
+
'staging ids unavailable: this diff has bytes that are not UTF-8 (a file in another encoding), and staging its text would replace them; stage those files whole with git add -- <paths>';
|
|
176
|
+
|
|
139
177
|
function fail(message) {
|
|
140
178
|
return `error: ${message}`;
|
|
141
179
|
}
|
|
@@ -398,6 +436,9 @@ async function runProcess(program, argv, { cwd, signal, maxBytes = MAX_CAPTURE_B
|
|
|
398
436
|
...(optionalLocks ? {} : { GIT_OPTIONAL_LOCKS: '0' }),
|
|
399
437
|
},
|
|
400
438
|
outputLimit: maxBytes,
|
|
439
|
+
// Bytes as git wrote them, decoded once by the caller: a multibyte
|
|
440
|
+
// character split across two pipe reads must not become U+FFFD.
|
|
441
|
+
rawOutput: true,
|
|
401
442
|
},
|
|
402
443
|
});
|
|
403
444
|
if (!native?.child)
|
|
@@ -561,9 +602,13 @@ function prepare(plan) {
|
|
|
561
602
|
return { argv: [operation, ...args] };
|
|
562
603
|
}
|
|
563
604
|
|
|
605
|
+
// A stage applies the text of this diff, so a patch with bytes that are not
|
|
606
|
+
// UTF-8 (a legacy-encoded file) would be applied with U+FFFD in their place.
|
|
607
|
+
// `notUtf8` marks such a diff: it is shown, but never staged by id.
|
|
564
608
|
async function runStageableDiff(plan, argv, repo, signal) {
|
|
565
609
|
const result = await runGit(plan, argv, { signal, readOnly: true });
|
|
566
610
|
if (!succeeded(result)) return result;
|
|
611
|
+
result.notUtf8 = !isUtf8(result.stdout);
|
|
567
612
|
result.stdout = String(result.stdout);
|
|
568
613
|
result.stderr = String(result.stderr);
|
|
569
614
|
const paths = plan.args.slice(1);
|
|
@@ -593,6 +638,7 @@ async function runStageableDiff(plan, argv, repo, signal) {
|
|
|
593
638
|
);
|
|
594
639
|
// --no-index uses exit 1 for a successfully produced patch.
|
|
595
640
|
if (!succeeded({ ...added, exitCode: added.exitCode === 1 ? 0 : added.exitCode })) return added;
|
|
641
|
+
if (!isUtf8(added.stdout)) result.notUtf8 = true;
|
|
596
642
|
result.stdout = appendText(result.stdout, String(added.stdout));
|
|
597
643
|
result.stderr = appendText(result.stderr, String(added.stderr));
|
|
598
644
|
}
|
|
@@ -763,26 +809,14 @@ function creationTarget(plan) {
|
|
|
763
809
|
// --prune=now as unsafe beside a concurrent writer), so these hold the whole
|
|
764
810
|
// repository.
|
|
765
811
|
const REPO_WIDE_OPERATIONS = new Set(['gc', 'maintenance', 'prune', 'prune-packed', 'repack']);
|
|
766
|
-
const WORKTREE_VALUE_FLAGS = new Set(['-b', '-B', '--reason']);
|
|
767
812
|
|
|
768
813
|
// The worktrees a mutating plan touches; null holds the whole repository. A
|
|
769
|
-
// write changes its cwd's worktree,
|
|
770
|
-
//
|
|
771
|
-
//
|
|
814
|
+
// write changes its cwd's worktree, so writes on different worktrees never
|
|
815
|
+
// wait for each other. (`git worktree` never runs as a git write here: it
|
|
816
|
+
// maps to managed operations, which take the repository lock themselves.)
|
|
772
817
|
function writeScopes(plan, root) {
|
|
773
818
|
if (REPO_WIDE_OPERATIONS.has(plan.operation)) return null;
|
|
774
|
-
|
|
775
|
-
const [action, ...paths] = optionFreePositionals(plan.args, WORKTREE_VALUE_FLAGS);
|
|
776
|
-
const target = (path) => resolve(plan.cwd, path);
|
|
777
|
-
// git also names a worktree by a unique path suffix, which no cwd-relative
|
|
778
|
-
// path resolves; an argument that is not a worktree here holds the whole
|
|
779
|
-
// repository instead of guessing (so do prune and an argument-less repair).
|
|
780
|
-
const existing = (path) => (existsSync(join(target(path), '.git')) ? target(path) : null);
|
|
781
|
-
let scopes = null;
|
|
782
|
-
if (action === 'add') scopes = paths.slice(0, 1).map(target);
|
|
783
|
-
else if (action === 'move' && paths.length === 2) scopes = [existing(paths[0]), target(paths[1])];
|
|
784
|
-
else if (['remove', 'lock', 'unlock', 'repair'].includes(action)) scopes = paths.map(existing);
|
|
785
|
-
return scopes?.length && scopes.every(Boolean) ? scopes : null;
|
|
819
|
+
return [root];
|
|
786
820
|
}
|
|
787
821
|
|
|
788
822
|
// A mutating git call holds the in-process and cross-process locks on its
|
|
@@ -804,6 +838,34 @@ function withGitWriteLocks(lockKey, scopes, root, signal, callback) {
|
|
|
804
838
|
);
|
|
805
839
|
}
|
|
806
840
|
|
|
841
|
+
// The managed snapshot worktree `repo` lies in, else null. A registry that
|
|
842
|
+
// cannot be read names none: commands elsewhere run as before.
|
|
843
|
+
function snapshotContaining(repo) {
|
|
844
|
+
let entries;
|
|
845
|
+
try {
|
|
846
|
+
entries = readRegistrySync().entries;
|
|
847
|
+
} catch {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
return (
|
|
851
|
+
Object.values(entries).find(
|
|
852
|
+
(record) => record?.kind === WORKTREE_SNAPSHOT_KIND && typeof record.path === 'string' && isWithin(record.path, repo)
|
|
853
|
+
) ?? null
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// A command run in a snapshot worktree is one of its consumers: the workspace
|
|
858
|
+
// lease it holds (activity-gate, the same as a background shell's) keeps the
|
|
859
|
+
// snapshot from being removed in any process until it ends, and a snapshot
|
|
860
|
+
// whose removal is under way refuses it. Returns the release.
|
|
861
|
+
function admitSnapshotCommand(repo, sessionId) {
|
|
862
|
+
const snapshot = snapshotContaining(repo);
|
|
863
|
+
if (!snapshot) return () => {};
|
|
864
|
+
const leave = tryAdmitWorkspaceActivity(sessionId || 'git-tool', repo, { kind: 'command' });
|
|
865
|
+
if (!leave) throw new Error(`the snapshot worktree ${snapshot.path} is being removed; nothing ran`);
|
|
866
|
+
return leave;
|
|
867
|
+
}
|
|
868
|
+
|
|
807
869
|
function invalidateAfterGitMutation() {
|
|
808
870
|
invalidateBuiltinResultCache();
|
|
809
871
|
drainCodeGraphCache();
|
|
@@ -873,6 +935,7 @@ async function executeGitStage(input, workDir, options = {}) {
|
|
|
873
935
|
};
|
|
874
936
|
const current = await runStageableDiff(snapshot.plan, snapshot.argv, repo, signal);
|
|
875
937
|
if (!succeeded(current)) return noted(commandFailure(snapshot.plan, current, limit));
|
|
938
|
+
if (current.notUtf8) return noted(fail(NOT_UTF8_STAGE_NOTE));
|
|
876
939
|
const raw = current.stdout;
|
|
877
940
|
if (!diffSnapshotMatches(snapshot, raw)) {
|
|
878
941
|
return answer({
|
|
@@ -919,6 +982,15 @@ async function executeSingleGitTool(input, workDir, options = {}) {
|
|
|
919
982
|
} catch (error) {
|
|
920
983
|
return rejected(error.message);
|
|
921
984
|
}
|
|
985
|
+
// No unmanaged worktree is ever made here: worktree commands run as their
|
|
986
|
+
// managed operations (git-worktree-tool.mjs); only `list --porcelain` stays git.
|
|
987
|
+
if (plan.operation === 'worktree') {
|
|
988
|
+
const mapped = await executeRawWorktree(plan, input, options);
|
|
989
|
+
if (mapped) {
|
|
990
|
+
if (!isReadOnly(plan)) invalidateAfterGitMutation();
|
|
991
|
+
return mapped;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
922
994
|
if (
|
|
923
995
|
plan.operation === 'archive' &&
|
|
924
996
|
!plan.args.some((value) => value === '-o' || value === '--output' || value.startsWith('--output='))
|
|
@@ -954,6 +1026,23 @@ async function executeSingleGitTool(input, workDir, options = {}) {
|
|
|
954
1026
|
const missing = probe ?? (await runGit(plan, ['rev-parse', '--show-toplevel'], { signal }));
|
|
955
1027
|
return commandResult(plan, missing, limit);
|
|
956
1028
|
}
|
|
1029
|
+
// A command in a snapshot worktree uses it like a worker would: it holds a
|
|
1030
|
+
// workspace lease there while it runs, so the snapshot is never removed
|
|
1031
|
+
// under it, and a snapshot being removed refuses it.
|
|
1032
|
+
let leaveSnapshot;
|
|
1033
|
+
try {
|
|
1034
|
+
leaveSnapshot = admitSnapshotCommand(repo, options.sessionId);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
return rejected(error.message);
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
return await runInRepository(input, plan, { repo, lockKey, limit, signal, workDir });
|
|
1040
|
+
} finally {
|
|
1041
|
+
leaveSnapshot();
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
async function runInRepository(input, plan, { repo, lockKey, limit, signal, workDir }) {
|
|
957
1046
|
const prepared = prepare(plan);
|
|
958
1047
|
const stageableRequest = plan.operation === 'diff' && (plan.args.length === 0 || plan.args[0] === '--');
|
|
959
1048
|
if (stageableRequest) prepared.argv.splice(1, 0, '--no-textconv', '--src-prefix=a/', '--dst-prefix=b/');
|
|
@@ -965,6 +1054,11 @@ async function executeSingleGitTool(input, workDir, options = {}) {
|
|
|
965
1054
|
: await runGit(plan, prepared.argv, { signal, readOnly: true });
|
|
966
1055
|
if (!succeeded(result)) return commandResult(plan, result, limit);
|
|
967
1056
|
if (stageableRequest && input.include_stage_ids === true) {
|
|
1057
|
+
if (result.notUtf8) {
|
|
1058
|
+
const rendered = commandResult(plan, result, limit);
|
|
1059
|
+
rendered.text = appendText(rendered.text, NOT_UTF8_STAGE_NOTE);
|
|
1060
|
+
return rendered;
|
|
1061
|
+
}
|
|
968
1062
|
const snapshot = createDiffSnapshot({
|
|
969
1063
|
repo,
|
|
970
1064
|
scope: resolve(workDir || process.cwd()),
|
|
@@ -977,6 +1071,9 @@ async function executeSingleGitTool(input, workDir, options = {}) {
|
|
|
977
1071
|
return commandResult(plan, result, limit);
|
|
978
1072
|
}
|
|
979
1073
|
return withGitWriteLocks(lockKey, writeScopes(plan, repo), repo, signal, async (waitedMs) => {
|
|
1074
|
+
// A managed removal holds the same folder lock while it deletes: once it
|
|
1075
|
+
// is ours, a folder that is gone was removed, and nothing runs there.
|
|
1076
|
+
if (!existsSync(repo)) return rejected(`the worktree folder ${repo} was removed meanwhile; nothing ran`);
|
|
980
1077
|
const result = await runGitWrite(plan, prepared.argv, { signal });
|
|
981
1078
|
invalidateAfterGitMutation();
|
|
982
1079
|
const rendered = commandResult(plan, result, limit);
|
|
@@ -1064,13 +1161,23 @@ export async function executeGitTool(rawInput, workDir, options = {}) {
|
|
|
1064
1161
|
if (rawInput.change_ids !== undefined) input.change_ids = parseStringifiedArray(rawInput.change_ids);
|
|
1065
1162
|
if (rawInput.command === undefined) delete input.command;
|
|
1066
1163
|
const action = input.action ?? 'command';
|
|
1164
|
+
if (action === 'worktree') {
|
|
1165
|
+
const text = await executeWorktreeAction(input, workDir, options);
|
|
1166
|
+
if (input.worktree_action !== 'list') invalidateAfterGitMutation();
|
|
1167
|
+
return text;
|
|
1168
|
+
}
|
|
1169
|
+
const worktreeField = WORKTREE_INPUT_FIELDS.find((field) => input[field] !== undefined);
|
|
1170
|
+
if (worktreeField) return fail(`git ${worktreeField} requires action:worktree`);
|
|
1171
|
+
if (input.confirm_large !== undefined && action !== 'command') {
|
|
1172
|
+
return fail('git confirm_large applies to action:worktree or a git worktree add command');
|
|
1173
|
+
}
|
|
1067
1174
|
if (action === 'stage') {
|
|
1068
1175
|
if (input.command !== undefined || input.include_stage_ids !== undefined) {
|
|
1069
1176
|
return fail('git action:stage accepts diff_id/change_ids, not command/include_stage_ids');
|
|
1070
1177
|
}
|
|
1071
1178
|
return executeGitStage(input, workDir, options);
|
|
1072
1179
|
}
|
|
1073
|
-
if (action !== 'command') return fail('git action must be command or
|
|
1180
|
+
if (action !== 'command') return fail('git action must be command, stage or worktree');
|
|
1074
1181
|
if (input.diff_id !== undefined || input.change_ids !== undefined) {
|
|
1075
1182
|
return fail('git diff_id/change_ids require action:stage');
|
|
1076
1183
|
}
|