baxian 1.2.60 → 1.2.61
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/dist/agent/dispatch-reconciler.js +6 -6
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +34 -4
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +628 -48
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/pane-streamer.js +1 -1
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +43 -2
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +301 -43
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +9 -0
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +63 -5
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts +20 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +217 -19
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +8 -0
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +45 -2
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +2 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +7 -3
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +8 -0
- package/dist/api/config.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +55 -4
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +26 -3
- package/dist/api/tasks.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/validator.js +8 -1
- package/dist/config/validator.js.map +1 -1
- package/dist/event/log.d.ts.map +1 -1
- package/dist/event/log.js +17 -2
- package/dist/event/log.js.map +1 -1
- package/dist/github/pr-conversation-cache.d.ts +11 -4
- package/dist/github/pr-conversation-cache.d.ts.map +1 -1
- package/dist/github/pr-conversation-cache.js +101 -7
- package/dist/github/pr-conversation-cache.js.map +1 -1
- package/dist/github/pr-conversation.d.ts +2 -2
- package/dist/github/pr-conversation.d.ts.map +1 -1
- package/dist/github/pr-conversation.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/platform/driver-spec.d.ts.map +1 -1
- package/dist/platform/driver-spec.js +33 -0
- package/dist/platform/driver-spec.js.map +1 -1
- package/dist/platform/git-driver.d.ts +7 -1
- package/dist/platform/git-driver.d.ts.map +1 -1
- package/dist/platform/git-driver.js +33 -2
- package/dist/platform/git-driver.js.map +1 -1
- package/dist/platform/platform-poller.d.ts +8 -0
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +80 -1
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/platform/plugin-registry.d.ts.map +1 -1
- package/dist/platform/plugin-registry.js +14 -2
- package/dist/platform/plugin-registry.js.map +1 -1
- package/dist/platform/plugins/github/driver.json +3 -0
- package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +16 -7
- package/dist/platform/preflight-exec.d.ts +21 -0
- package/dist/platform/preflight-exec.d.ts.map +1 -0
- package/dist/platform/preflight-exec.js +62 -0
- package/dist/platform/preflight-exec.js.map +1 -0
- package/dist/platform/review-timeline.d.ts +16 -0
- package/dist/platform/review-timeline.d.ts.map +1 -0
- package/dist/platform/review-timeline.js +195 -0
- package/dist/platform/review-timeline.js.map +1 -0
- package/dist/platform/startup.d.ts +5 -1
- package/dist/platform/startup.d.ts.map +1 -1
- package/dist/platform/startup.js +25 -0
- package/dist/platform/startup.js.map +1 -1
- package/dist/platform/types.d.ts +2 -0
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/types.js +1 -1
- package/dist/platform/types.js.map +1 -1
- package/dist/shared/git-url.d.ts +1 -0
- package/dist/shared/git-url.d.ts.map +1 -1
- package/dist/shared/git-url.js +3 -0
- package/dist/shared/git-url.js.map +1 -1
- package/dist/shared/types.d.ts +19 -6
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/skill/registry.d.ts +9 -2
- package/dist/skill/registry.d.ts.map +1 -1
- package/dist/skill/registry.js +88 -54
- package/dist/skill/registry.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +2 -0
- package/dist/skills/baxian-pr-recheck/SKILL.md +2 -0
- package/dist/skills/baxian-pr-review/SKILL.md +2 -0
- package/dist/skills/baxian-research/SKILL.md +1 -1
- package/dist/skills/baxian-signals/SKILL.md +19 -4
- package/dist/skills/baxian-task-check/SKILL.md +4 -2
- package/dist/state/agent-store.d.ts.map +1 -1
- package/dist/state/agent-store.js +27 -2
- package/dist/state/agent-store.js.map +1 -1
- package/dist/state/task-store.js +3 -3
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js +7 -6
- package/dist/terminal/attach.js.map +1 -1
- package/dist/web/assets/{index-CwYBVTyn.css → index-BPv6vv3U.css} +1 -1
- package/dist/web/assets/index-CUJtd0bC.js +16 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DtnDvjOU.js +0 -16
package/dist/agent/manager.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createSignalToken } from './phase-signal.js';
|
|
|
6
6
|
import { tmuxInstallHint } from './preflight.js';
|
|
7
7
|
import { BRANCH_PREFIX, isValidBranchName, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, TASK_OWNER_ROLES, isSpecStagePhase, isGitHubRepo, parseGitRemote, repoIdentityKey, repoSlug, } from '../shared/index.js';
|
|
8
8
|
import { buildProjectDriver, makeDriverExec } from '../platform/driver-host.js';
|
|
9
|
-
import { DriverOpError } from '../platform/git-driver.js';
|
|
9
|
+
import { DriverOpError, buildDriverRunContext, GitDriver } from '../platform/git-driver.js';
|
|
10
10
|
import { checkOpenPrBinding } from '../platform/pr-binding.js';
|
|
11
11
|
import { collectPendingFeedback, scanCommentSourcesOnce } from '../platform/feedback.js';
|
|
12
12
|
import { recheckPassProvenance } from '../platform/verdict-engine.js';
|
|
@@ -20,12 +20,12 @@ import { imageFilename, agentHostPath, writeImageToHost } from './image-input.js
|
|
|
20
20
|
import { TmuxManager, ReplNotReadyError, detectStartupDialog, detectRuntimeMenu, runtimeBusyCheck, hasRuntimeReadyView, hasReplProcTitle, hasOscTitleWorking, hasOscTitleIdle, screenAllowsTitleIdle, PaneGoneError, } from './tmux.js';
|
|
21
21
|
import { BranchManager, DirtyWorkdirError, ReviewHeadMismatchError, isAutoDeletableTaskBranch } from './branch.js';
|
|
22
22
|
import { RepoStore, createRepoStoreCache, moveFileIntoPlace, stageFileGuarded, canonicalSelfGuard } from './repo-store.js';
|
|
23
|
-
import { PhaseSignalWatcher } from './phase-signal-watcher.js';
|
|
23
|
+
import { PhaseSignalWatcher, } from './phase-signal-watcher.js';
|
|
24
24
|
import { ReviewTransport, resolveServerPayloads } from './review-transport.js';
|
|
25
25
|
import { buildPromptInline, buildGreetingPrompt, PromptSizeError, RequiredSkillsMissingError, MAX_PROMPT_BYTES_ROUTE_LIMIT, } from './prompt.js';
|
|
26
26
|
import { ApiError } from '../errors.js';
|
|
27
27
|
import { prepareConfig } from '../config/loader.js';
|
|
28
|
-
import { resolveProjectTool } from '../config/validator.js';
|
|
28
|
+
import { projectReviewMode, resolveProjectTool } from '../config/validator.js';
|
|
29
29
|
import { SHA_HEX_SOURCE } from '../platform/types.js';
|
|
30
30
|
export class EnsureSessionError extends Error {
|
|
31
31
|
partial;
|
|
@@ -146,6 +146,7 @@ export function skillSubdirFor(runtime) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
const DEFAULT_DISPATCH_ACK_TIMEOUT_MS = 30_000;
|
|
149
|
+
const DEFAULT_NEED_INPUT_RETRY_INTERVAL_MS = 5_000;
|
|
149
150
|
const DEFAULT_DISPATCH_SETTLE_TIMEOUT_MS = 3_000;
|
|
150
151
|
// DELETE runtime cleanup runs while holding the deletion tombstone + rotated lock; an unbounded remote
|
|
151
152
|
// probe/kill would block rollback/finally forever, so every tmux round-trip in it carries this deadline.
|
|
@@ -233,6 +234,7 @@ export class AgentManager {
|
|
|
233
234
|
paneStreamerManager;
|
|
234
235
|
postApproveStore;
|
|
235
236
|
pluginRegistry;
|
|
237
|
+
platformDefaultBranchOf;
|
|
236
238
|
platformDriverExec;
|
|
237
239
|
phaseSignalWatcher;
|
|
238
240
|
errorRecordStore;
|
|
@@ -240,6 +242,7 @@ export class AgentManager {
|
|
|
240
242
|
reviewTransportInstance;
|
|
241
243
|
serverReviewDriver;
|
|
242
244
|
dispatchAckTimeoutMs;
|
|
245
|
+
needInputRetryIntervalMs;
|
|
243
246
|
dispatchSettleTimeoutMs;
|
|
244
247
|
dispatchAckResendIntervalMs = 3_000;
|
|
245
248
|
taskMutationQueue = Promise.resolve();
|
|
@@ -283,6 +286,7 @@ export class AgentManager {
|
|
|
283
286
|
this.paneStreamerManager = deps.paneStreamerManager;
|
|
284
287
|
this.postApproveStore = deps.postApproveStore ?? new PostApproveStore();
|
|
285
288
|
this.pluginRegistry = deps.pluginRegistry;
|
|
289
|
+
this.platformDefaultBranchOf = deps.platformDefaultBranchOf;
|
|
286
290
|
this.errorRecordStore = deps.errorRecordStore;
|
|
287
291
|
this.phaseSignalWatcher = deps.phaseSignalWatcher
|
|
288
292
|
?? (deps.paneStreamerManager
|
|
@@ -290,8 +294,10 @@ export class AgentManager {
|
|
|
290
294
|
paneStreamerManager: deps.paneStreamerManager,
|
|
291
295
|
eventBus: deps.eventBus,
|
|
292
296
|
resolveAgent: (id) => this.getAgentConfig(id),
|
|
297
|
+
commitNeedInputWatermark: (intent) => this.commitNeedInputWatermark(intent),
|
|
293
298
|
})
|
|
294
299
|
: undefined);
|
|
300
|
+
this.needInputRetryIntervalMs = deps.needInputRetryIntervalMs ?? DEFAULT_NEED_INPUT_RETRY_INTERVAL_MS;
|
|
295
301
|
this.reviewStore = deps.reviewStore;
|
|
296
302
|
this.dispatchAckTimeoutMs = deps.dispatchAckTimeoutMs ?? DEFAULT_DISPATCH_ACK_TIMEOUT_MS;
|
|
297
303
|
this.dispatchSettleTimeoutMs = deps.dispatchSettleTimeoutMs ?? DEFAULT_DISPATCH_SETTLE_TIMEOUT_MS;
|
|
@@ -592,9 +598,19 @@ export class AgentManager {
|
|
|
592
598
|
}
|
|
593
599
|
return this.adoptOrRestartSessionLocked(tmux, runner, agent, agentId, workdir, workdir, snapshot);
|
|
594
600
|
}
|
|
601
|
+
agentSkillScope(agentId) {
|
|
602
|
+
const projectId = this.getAgentConfig(agentId)?.projectId;
|
|
603
|
+
if (projectId === undefined || this.effectiveReviewMode(projectId) !== 'git') {
|
|
604
|
+
return { pluginTools: [] };
|
|
605
|
+
}
|
|
606
|
+
const project = this.getProjectConfig(projectId);
|
|
607
|
+
const tool = project === undefined ? undefined : resolveProjectTool(project);
|
|
608
|
+
return { pluginTools: tool === undefined ? [] : [tool] };
|
|
609
|
+
}
|
|
595
610
|
async provisionRepoSkills(runner, agent, workdir) {
|
|
596
611
|
if (this.skillRegistry.names().length === 0)
|
|
597
612
|
return;
|
|
613
|
+
const scope = this.agentSkillScope(agent.id);
|
|
598
614
|
const subdir = skillSubdirFor(agent.runtime);
|
|
599
615
|
const destRoot = `${workdir}/${subdir}`;
|
|
600
616
|
await this.excludeInjectedSkills(runner, workdir, subdir);
|
|
@@ -604,8 +620,11 @@ export class AgentManager {
|
|
|
604
620
|
// would execute a `!`cmd`` / inline a @path embedded in an untrusted task title/description
|
|
605
621
|
// before the skill runs. Message-body payloads are not expanded, so this path is injection-safe.
|
|
606
622
|
await this.runUnderSkillDirLock(this.skillDirLockKey(agent, workdir), async () => {
|
|
607
|
-
await this.ensureSkillDirSafe(runner, workdir, subdir
|
|
608
|
-
|
|
623
|
+
await this.ensureSkillDirSafe(runner, workdir, subdir, [
|
|
624
|
+
...this.skillRegistry.names(),
|
|
625
|
+
...this.skillRegistry.pluginSkillNames(scope),
|
|
626
|
+
]);
|
|
627
|
+
await this.skillRegistry.materialize((path, content) => this.atomicWriteFile(runner, workdir, path, content), destRoot, scope);
|
|
609
628
|
});
|
|
610
629
|
}
|
|
611
630
|
// A rollback only kills if no adoption bumped the generation since its create.
|
|
@@ -706,9 +725,9 @@ export class AgentManager {
|
|
|
706
725
|
throw new Error(`atomic skill write failed (${finalPath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
707
726
|
}
|
|
708
727
|
}
|
|
709
|
-
async ensureSkillDirSafe(runner, workdir, subdir) {
|
|
728
|
+
async ensureSkillDirSafe(runner, workdir, subdir, keepNames) {
|
|
710
729
|
const top = subdir.split('/')[0];
|
|
711
|
-
const keep =
|
|
730
|
+
const keep = keepNames.map((n) => `! -name ${shellQuote(n)}`).join(' ');
|
|
712
731
|
const inner =
|
|
713
732
|
// canonicalSelfGuard (cd+pwd -P) rejects a rebound MIDDLE ancestor, not just a symlinked leaf, so the find/rm can't escape.
|
|
714
733
|
`${canonicalSelfGuard(workdir)} && ` +
|
|
@@ -729,14 +748,14 @@ export class AgentManager {
|
|
|
729
748
|
if (this.skillRegistry.names().length === 0)
|
|
730
749
|
return;
|
|
731
750
|
await this.setSessionOptions(tmux, agentId, ref, [
|
|
732
|
-
['@baxian-skills-version', this.skillRegistry.contentHash()],
|
|
751
|
+
['@baxian-skills-version', this.skillRegistry.contentHash(this.agentSkillScope(agentId))],
|
|
733
752
|
]);
|
|
734
753
|
}
|
|
735
754
|
async replSkillsStale(tmux, agentId, ref) {
|
|
736
755
|
if (this.skillRegistry.names().length === 0)
|
|
737
756
|
return false;
|
|
738
757
|
const tagged = await tmux.getSessionOptionByRef(ref, agentId, '@baxian-skills-version');
|
|
739
|
-
return tagged !== this.skillRegistry.contentHash();
|
|
758
|
+
return tagged !== this.skillRegistry.contentHash(this.agentSkillScope(agentId));
|
|
740
759
|
}
|
|
741
760
|
async excludeInjectedSkills(runner, workdir, subdir) {
|
|
742
761
|
const inner =
|
|
@@ -1745,11 +1764,15 @@ export class AgentManager {
|
|
|
1745
1764
|
...(latest.workdir !== undefined ? { workdir: latest.workdir } : {}),
|
|
1746
1765
|
...(latest.startedAt !== undefined ? { startedAt: latest.startedAt } : {}),
|
|
1747
1766
|
...(latest.creationToken !== undefined ? { creationToken: latest.creationToken } : {}),
|
|
1767
|
+
// Release voids the open question: strip the watermark, bump the epoch so
|
|
1768
|
+
// any in-flight/queued commit of the old generation fences server-side.
|
|
1769
|
+
needInput: { epoch: (latest.needInput?.epoch ?? 0) + 1 },
|
|
1748
1770
|
};
|
|
1749
1771
|
return cleared;
|
|
1750
1772
|
}
|
|
1751
1773
|
return {
|
|
1752
1774
|
...latest,
|
|
1775
|
+
needInput: { epoch: (latest.needInput?.epoch ?? 0) + 1 },
|
|
1753
1776
|
updatedAt: now,
|
|
1754
1777
|
};
|
|
1755
1778
|
});
|
|
@@ -1757,6 +1780,7 @@ export class AgentManager {
|
|
|
1757
1780
|
console.warn(`[AgentManager] releaseAgentForTask: agent ${agentId} holds a newer generation than the captured one; keeping the hold`);
|
|
1758
1781
|
return false;
|
|
1759
1782
|
}
|
|
1783
|
+
this.clearNeedInputRetryFor(agentId, expectedTaskId);
|
|
1760
1784
|
return true;
|
|
1761
1785
|
}
|
|
1762
1786
|
let sessionConfirmedAbsent = false;
|
|
@@ -1951,6 +1975,7 @@ export class AgentManager {
|
|
|
1951
1975
|
...(existing.workdir !== undefined ? { workdir: existing.workdir } : {}),
|
|
1952
1976
|
...(!sessionConfirmedAbsent && existing.paneId !== undefined ? { paneId: existing.paneId } : {}),
|
|
1953
1977
|
...(existing.creationToken !== undefined ? { creationToken: existing.creationToken } : {}),
|
|
1978
|
+
needInput: { epoch: (existing.needInput?.epoch ?? 0) + 1 },
|
|
1954
1979
|
updatedAt: now,
|
|
1955
1980
|
};
|
|
1956
1981
|
});
|
|
@@ -1958,6 +1983,7 @@ export class AgentManager {
|
|
|
1958
1983
|
console.warn(`[AgentManager] releaseAgentForTask: agent ${agentId} hold generation changed during release; keeping the hold`);
|
|
1959
1984
|
return false;
|
|
1960
1985
|
}
|
|
1986
|
+
this.clearNeedInputRetryFor(agentId, expectedTaskId);
|
|
1961
1987
|
return this.lockManager.releaseIfOwner(agentId, expectedTaskId, lockToken);
|
|
1962
1988
|
});
|
|
1963
1989
|
}
|
|
@@ -2143,48 +2169,469 @@ export class AgentManager {
|
|
|
2143
2169
|
}
|
|
2144
2170
|
}
|
|
2145
2171
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2172
|
+
// ---- need-input watermark: epoch-fenced monotonic persistence + retry convergence ----
|
|
2173
|
+
needInputRetry = new Map();
|
|
2174
|
+
needInputRetryInFlight = new Set();
|
|
2175
|
+
// Watermark writes currently in the store chain. A restore bump snapshots these along
|
|
2176
|
+
// with the queue: an answer whose write has not settled yet would otherwise be invisible
|
|
2177
|
+
// to the migration and its error-enqueue would land after the arm already cleared the
|
|
2178
|
+
// queue — deleting the only proof the user answered.
|
|
2179
|
+
needInputWritesInFlight = new Map();
|
|
2180
|
+
needInputWriteSeq = 0;
|
|
2181
|
+
// Serializes the probe→bump→queue-clear section of concurrent arms for one
|
|
2182
|
+
// (agentId, taskId): without it a stale replay probed before a successor's bump can
|
|
2183
|
+
// itself bump afterwards, ghost-fencing the installed successor. All locked steps are
|
|
2184
|
+
// bounded (memory probe + one store update); subscribe stays outside the lock.
|
|
2185
|
+
needInputArmLocks = new Map();
|
|
2186
|
+
needInputRetryTimer = null;
|
|
2187
|
+
needInputRetryKey(agentId, taskId, epoch) {
|
|
2188
|
+
return `${agentId} ${taskId} ${epoch}`;
|
|
2189
|
+
}
|
|
2190
|
+
parseNeedInputRetryKey(key) {
|
|
2191
|
+
const [agentId, taskId, epochRaw] = key.split(' ');
|
|
2192
|
+
if (!agentId || !taskId || epochRaw === undefined)
|
|
2193
|
+
return null;
|
|
2194
|
+
const epoch = Number.parseInt(epochRaw, 10);
|
|
2195
|
+
if (!Number.isFinite(epoch))
|
|
2196
|
+
return null;
|
|
2197
|
+
return { agentId, taskId, epoch };
|
|
2198
|
+
}
|
|
2199
|
+
// Standard commit entry: one agentStore.update, NEVER called from inside another
|
|
2200
|
+
// updater (the store's per-id chain is non-reentrant).
|
|
2201
|
+
// The ledger entry lives until the result is fully handed over — an error is IN the
|
|
2202
|
+
// retry queue before deregistration, in the same synchronous continuation. Splitting
|
|
2203
|
+
// those two across promise jobs would open a window where neither the ledger nor the
|
|
2204
|
+
// queue holds the intent and a concurrent restore snapshot loses the answer.
|
|
2205
|
+
async commitNeedInputWatermark(intent) {
|
|
2206
|
+
const writeId = ++this.needInputWriteSeq;
|
|
2207
|
+
this.needInputWritesInFlight.set(writeId, intent);
|
|
2208
|
+
const key = this.needInputRetryKey(intent.agentId, intent.taskId, intent.epoch);
|
|
2148
2209
|
try {
|
|
2149
|
-
await this.
|
|
2150
|
-
|
|
2210
|
+
const result = await this.writeNeedInputWatermark(intent);
|
|
2211
|
+
if (result === 'error') {
|
|
2212
|
+
this.enqueueNeedInputRetry(key, intent.askSeq, intent.answeredSeq);
|
|
2213
|
+
}
|
|
2214
|
+
else {
|
|
2215
|
+
this.settleNeedInputRetry(key, intent.askSeq, intent.answeredSeq, result);
|
|
2216
|
+
}
|
|
2217
|
+
return result;
|
|
2218
|
+
}
|
|
2219
|
+
catch (err) {
|
|
2220
|
+
this.enqueueNeedInputRetry(key, intent.askSeq, intent.answeredSeq);
|
|
2221
|
+
throw err;
|
|
2222
|
+
}
|
|
2223
|
+
finally {
|
|
2224
|
+
this.needInputWritesInFlight.delete(writeId);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async writeNeedInputWatermark(intent) {
|
|
2228
|
+
let outcome = 'fenced';
|
|
2229
|
+
try {
|
|
2230
|
+
await this.agentStore.update(intent.agentId, (existing) => {
|
|
2231
|
+
if (!existing || existing.taskId !== intent.taskId) {
|
|
2232
|
+
outcome = 'fenced';
|
|
2151
2233
|
return AGENT_STORE_NOOP;
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
if (
|
|
2234
|
+
}
|
|
2235
|
+
const cur = existing.needInput;
|
|
2236
|
+
if ((cur?.epoch ?? 0) !== intent.epoch) {
|
|
2237
|
+
outcome = 'fenced';
|
|
2155
2238
|
return AGENT_STORE_NOOP;
|
|
2156
|
-
if (pending) {
|
|
2157
|
-
if (existing.needInputAt !== undefined)
|
|
2158
|
-
return AGENT_STORE_NOOP;
|
|
2159
|
-
return { ...existing, needInputAt: now, updatedAt: now };
|
|
2160
2239
|
}
|
|
2161
|
-
|
|
2240
|
+
const askSeq = Math.max(cur?.askSeq ?? 0, intent.askSeq);
|
|
2241
|
+
const answeredSeq = Math.max(cur?.answeredSeq ?? 0, intent.answeredSeq);
|
|
2242
|
+
outcome = 'ok';
|
|
2243
|
+
if (cur && (cur.askSeq ?? 0) === askSeq && (cur.answeredSeq ?? 0) === answeredSeq) {
|
|
2162
2244
|
return AGENT_STORE_NOOP;
|
|
2163
|
-
|
|
2164
|
-
|
|
2245
|
+
}
|
|
2246
|
+
const now = new Date().toISOString();
|
|
2247
|
+
const at = askSeq > answeredSeq ? (cur?.at ?? now) : undefined;
|
|
2248
|
+
return {
|
|
2249
|
+
...existing,
|
|
2250
|
+
needInput: {
|
|
2251
|
+
epoch: intent.epoch, askSeq, answeredSeq,
|
|
2252
|
+
...(at !== undefined ? { at } : {}),
|
|
2253
|
+
...(cur?.cutoverToken !== undefined ? { cutoverToken: cur.cutoverToken } : {}),
|
|
2254
|
+
},
|
|
2255
|
+
updatedAt: now,
|
|
2256
|
+
};
|
|
2165
2257
|
});
|
|
2166
2258
|
}
|
|
2167
2259
|
catch (err) {
|
|
2168
|
-
console.warn(`[AgentManager]
|
|
2260
|
+
console.warn(`[AgentManager] needInput watermark write failed (agent=${intent.agentId} epoch=${intent.epoch}):`, err);
|
|
2261
|
+
return 'error';
|
|
2262
|
+
}
|
|
2263
|
+
return outcome;
|
|
2264
|
+
}
|
|
2265
|
+
enqueueNeedInputRetry(key, askSeq, answeredSeq) {
|
|
2266
|
+
const cur = this.needInputRetry.get(key);
|
|
2267
|
+
this.needInputRetry.set(key, {
|
|
2268
|
+
askSeq: Math.max(cur?.askSeq ?? 0, askSeq),
|
|
2269
|
+
answeredSeq: Math.max(cur?.answeredSeq ?? 0, answeredSeq),
|
|
2270
|
+
});
|
|
2271
|
+
this.ensureNeedInputRetryTimer();
|
|
2272
|
+
}
|
|
2273
|
+
// ok deletes only when the queued item is still covered by what was written
|
|
2274
|
+
// (a concurrent edge may have raised it mid-flight); fenced is terminal either way.
|
|
2275
|
+
settleNeedInputRetry(key, wroteAskSeq, wroteAnsweredSeq, result) {
|
|
2276
|
+
const cur = this.needInputRetry.get(key);
|
|
2277
|
+
if (!cur)
|
|
2278
|
+
return;
|
|
2279
|
+
if (result === 'fenced' || (cur.askSeq <= wroteAskSeq && cur.answeredSeq <= wroteAnsweredSeq)) {
|
|
2280
|
+
this.needInputRetry.delete(key);
|
|
2281
|
+
this.ensureNeedInputRetryTimer();
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
clearNeedInputRetryFor(agentId, taskId) {
|
|
2285
|
+
for (const key of [...this.needInputRetry.keys()]) {
|
|
2286
|
+
const parsed = this.parseNeedInputRetryKey(key);
|
|
2287
|
+
if (parsed && parsed.agentId === agentId && parsed.taskId === taskId) {
|
|
2288
|
+
this.needInputRetry.delete(key);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
this.ensureNeedInputRetryTimer();
|
|
2292
|
+
}
|
|
2293
|
+
ensureNeedInputRetryTimer() {
|
|
2294
|
+
if (this.needInputRetry.size === 0) {
|
|
2295
|
+
if (this.needInputRetryTimer) {
|
|
2296
|
+
clearInterval(this.needInputRetryTimer);
|
|
2297
|
+
this.needInputRetryTimer = null;
|
|
2298
|
+
}
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
if (this.needInputRetryTimer)
|
|
2302
|
+
return;
|
|
2303
|
+
this.needInputRetryTimer = setInterval(() => {
|
|
2304
|
+
void this.needInputRetryPass();
|
|
2305
|
+
}, this.needInputRetryIntervalMs);
|
|
2306
|
+
this.needInputRetryTimer.unref?.();
|
|
2307
|
+
}
|
|
2308
|
+
async needInputRetryPass() {
|
|
2309
|
+
for (const [key, snapshot] of [...this.needInputRetry.entries()]) {
|
|
2310
|
+
if (this.needInputRetryInFlight.has(key))
|
|
2311
|
+
continue;
|
|
2312
|
+
const parsed = this.parseNeedInputRetryKey(key);
|
|
2313
|
+
if (!parsed) {
|
|
2314
|
+
this.needInputRetry.delete(key);
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
this.needInputRetryInFlight.add(key);
|
|
2318
|
+
try {
|
|
2319
|
+
const result = await this.writeNeedInputWatermark({
|
|
2320
|
+
agentId: parsed.agentId,
|
|
2321
|
+
taskId: parsed.taskId,
|
|
2322
|
+
epoch: parsed.epoch,
|
|
2323
|
+
askSeq: snapshot.askSeq,
|
|
2324
|
+
answeredSeq: snapshot.answeredSeq,
|
|
2325
|
+
});
|
|
2326
|
+
if (result !== 'error') {
|
|
2327
|
+
this.settleNeedInputRetry(key, snapshot.askSeq, snapshot.answeredSeq, result);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
finally {
|
|
2331
|
+
this.needInputRetryInFlight.delete(key);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
this.ensureNeedInputRetryTimer();
|
|
2335
|
+
}
|
|
2336
|
+
collectSameEpochNeedInput(agentId, taskId, epoch) {
|
|
2337
|
+
let owed = null;
|
|
2338
|
+
const merge = (wm) => {
|
|
2339
|
+
owed = {
|
|
2340
|
+
askSeq: Math.max(owed?.askSeq ?? 0, wm.askSeq),
|
|
2341
|
+
answeredSeq: Math.max(owed?.answeredSeq ?? 0, wm.answeredSeq),
|
|
2342
|
+
};
|
|
2343
|
+
};
|
|
2344
|
+
for (const [key, wm] of this.needInputRetry) {
|
|
2345
|
+
const parsed = this.parseNeedInputRetryKey(key);
|
|
2346
|
+
if (!parsed || parsed.agentId !== agentId || parsed.taskId !== taskId)
|
|
2347
|
+
continue;
|
|
2348
|
+
if (parsed.epoch === epoch)
|
|
2349
|
+
merge(wm);
|
|
2350
|
+
}
|
|
2351
|
+
for (const intent of this.needInputWritesInFlight.values()) {
|
|
2352
|
+
if (intent.agentId !== agentId || intent.taskId !== taskId)
|
|
2353
|
+
continue;
|
|
2354
|
+
if (intent.epoch === epoch)
|
|
2355
|
+
merge(intent);
|
|
2356
|
+
}
|
|
2357
|
+
return owed;
|
|
2358
|
+
}
|
|
2359
|
+
// Only the generation this arm directly succeeds (bumps are +1 and serialized per key).
|
|
2360
|
+
// Anything older was already superseded — a fresh arm in between stripped its question
|
|
2361
|
+
// on purpose, and transplanting it would relight a prompt nobody asked.
|
|
2362
|
+
collectPredecessorNeedInputDelta(agentId, taskId, epoch) {
|
|
2363
|
+
let delta = null;
|
|
2364
|
+
const merge = (wm) => {
|
|
2365
|
+
delta = {
|
|
2366
|
+
askSeq: Math.max(delta?.askSeq ?? 0, wm.askSeq),
|
|
2367
|
+
answeredSeq: Math.max(delta?.answeredSeq ?? 0, wm.answeredSeq),
|
|
2368
|
+
};
|
|
2369
|
+
};
|
|
2370
|
+
for (const [key, wm] of this.needInputRetry) {
|
|
2371
|
+
const parsed = this.parseNeedInputRetryKey(key);
|
|
2372
|
+
if (!parsed || parsed.agentId !== agentId || parsed.taskId !== taskId)
|
|
2373
|
+
continue;
|
|
2374
|
+
if (parsed.epoch === epoch - 1)
|
|
2375
|
+
merge(wm);
|
|
2376
|
+
}
|
|
2377
|
+
for (const intent of this.needInputWritesInFlight.values()) {
|
|
2378
|
+
if (intent.agentId !== agentId || intent.taskId !== taskId)
|
|
2379
|
+
continue;
|
|
2380
|
+
if (intent.epoch === epoch - 1)
|
|
2381
|
+
merge(intent);
|
|
2169
2382
|
}
|
|
2383
|
+
return delta;
|
|
2384
|
+
}
|
|
2385
|
+
async withNeedInputArmLock(agentId, taskId, fn) {
|
|
2386
|
+
const key = `${agentId} ${taskId}`;
|
|
2387
|
+
const prev = this.needInputArmLocks.get(key) ?? Promise.resolve();
|
|
2388
|
+
const run = prev.then(fn);
|
|
2389
|
+
const settled = run.then(() => undefined, () => undefined);
|
|
2390
|
+
this.needInputArmLocks.set(key, settled);
|
|
2391
|
+
void settled.finally(() => {
|
|
2392
|
+
if (this.needInputArmLocks.get(key) === settled)
|
|
2393
|
+
this.needInputArmLocks.delete(key);
|
|
2394
|
+
});
|
|
2395
|
+
return run;
|
|
2170
2396
|
}
|
|
2171
|
-
//
|
|
2172
|
-
//
|
|
2173
|
-
//
|
|
2174
|
-
|
|
2397
|
+
// 'restore' must also merge queued retry seqs of the CURRENT persisted epoch: after
|
|
2398
|
+
// session-gone the queue can hold the only proof an answer happened (the entry is
|
|
2399
|
+
// gone, so needInputInherit has nothing to merge), and the arm clears the queue.
|
|
2400
|
+
async bumpNeedInputEpochForArm(agentId, taskId, mode, opts = {}) {
|
|
2401
|
+
// Contributions are collected INSIDE the updater: it runs when the store chain
|
|
2402
|
+
// reaches this bump, so an answer whose commit started while the bump was queued
|
|
2403
|
+
// (registered in the ledger, or already error-queued) is still visible. A snapshot
|
|
2404
|
+
// taken at call time would miss exactly that window.
|
|
2405
|
+
const collectContributions = () => {
|
|
2406
|
+
const queued = new Map();
|
|
2407
|
+
if (mode !== 'restore')
|
|
2408
|
+
return queued;
|
|
2409
|
+
const mergeContribution = (epoch, wm) => {
|
|
2410
|
+
const cur = queued.get(epoch);
|
|
2411
|
+
queued.set(epoch, {
|
|
2412
|
+
askSeq: Math.max(cur?.askSeq ?? 0, wm.askSeq),
|
|
2413
|
+
answeredSeq: Math.max(cur?.answeredSeq ?? 0, wm.answeredSeq),
|
|
2414
|
+
});
|
|
2415
|
+
};
|
|
2416
|
+
for (const [key, qwm] of this.needInputRetry) {
|
|
2417
|
+
const parsed = this.parseNeedInputRetryKey(key);
|
|
2418
|
+
if (!parsed || parsed.agentId !== agentId || parsed.taskId !== taskId)
|
|
2419
|
+
continue;
|
|
2420
|
+
mergeContribution(parsed.epoch, qwm);
|
|
2421
|
+
}
|
|
2422
|
+
for (const intent of this.needInputWritesInFlight.values()) {
|
|
2423
|
+
if (intent.agentId !== agentId || intent.taskId !== taskId)
|
|
2424
|
+
continue;
|
|
2425
|
+
mergeContribution(intent.epoch, intent);
|
|
2426
|
+
}
|
|
2427
|
+
return queued;
|
|
2428
|
+
};
|
|
2429
|
+
let wm = null;
|
|
2430
|
+
let snapshotBlind = false;
|
|
2431
|
+
// Scoped to THIS arm's token: a marker left by another token's replay says nothing
|
|
2432
|
+
// about the pane's ambiguity for us, so it is neither honoured nor carried forward.
|
|
2433
|
+
const keepCutover = (cur) => (opts.paneCutover
|
|
2434
|
+
? opts.armToken
|
|
2435
|
+
: cur !== undefined && cur.cutoverToken === opts.armToken ? cur.cutoverToken : undefined);
|
|
2436
|
+
try {
|
|
2437
|
+
await this.agentStore.update(agentId, (existing) => {
|
|
2438
|
+
if (!existing || existing.taskId !== taskId)
|
|
2439
|
+
return AGENT_STORE_NOOP;
|
|
2440
|
+
const cur = existing.needInput;
|
|
2441
|
+
const contrib = collectContributions().get(cur?.epoch ?? 0);
|
|
2442
|
+
const epoch = (cur?.epoch ?? 0) + 1;
|
|
2443
|
+
const askSeq = mode === 'restore' ? Math.max(cur?.askSeq ?? 0, contrib?.askSeq ?? 0) : 0;
|
|
2444
|
+
const answeredSeq = mode === 'restore'
|
|
2445
|
+
? Math.max(cur?.answeredSeq ?? 0, contrib?.answeredSeq ?? 0)
|
|
2446
|
+
: 0;
|
|
2447
|
+
wm = { epoch, askSeq, answeredSeq };
|
|
2448
|
+
const cutoverToken = keepCutover(cur);
|
|
2449
|
+
snapshotBlind = cutoverToken !== undefined;
|
|
2450
|
+
const at = askSeq > answeredSeq ? (cur?.at ?? new Date().toISOString()) : undefined;
|
|
2451
|
+
return {
|
|
2452
|
+
...existing,
|
|
2453
|
+
needInput: {
|
|
2454
|
+
epoch, askSeq, answeredSeq,
|
|
2455
|
+
...(at !== undefined ? { at } : {}),
|
|
2456
|
+
...(cutoverToken !== undefined ? { cutoverToken } : {}),
|
|
2457
|
+
},
|
|
2458
|
+
updatedAt: new Date().toISOString(),
|
|
2459
|
+
};
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
catch (err) {
|
|
2463
|
+
console.warn(`[AgentManager] needInput epoch bump failed (agent=${agentId} task=${taskId}):`, err);
|
|
2464
|
+
// Fall back to the persisted watermark for a restore: without it the entry would
|
|
2465
|
+
// start at 0/0 and swallow the answer to a question that is still open in the
|
|
2466
|
+
// store, leaving the badge lit forever. No bump means no generation advance —
|
|
2467
|
+
// the caller must not treat this as a fresh generation.
|
|
2468
|
+
if (mode === 'restore') {
|
|
2469
|
+
try {
|
|
2470
|
+
const cur = await this.agentStore.get(agentId);
|
|
2471
|
+
if (cur?.taskId === taskId && cur.needInput) {
|
|
2472
|
+
// Same-generation intents still owed to the store hold questions the
|
|
2473
|
+
// persisted watermark has not caught up with; merge them in.
|
|
2474
|
+
const owed = this.collectSameEpochNeedInput(agentId, taskId, cur.needInput.epoch);
|
|
2475
|
+
return {
|
|
2476
|
+
wm: {
|
|
2477
|
+
epoch: cur.needInput.epoch,
|
|
2478
|
+
askSeq: Math.max(cur.needInput.askSeq ?? 0, owed?.askSeq ?? 0),
|
|
2479
|
+
answeredSeq: Math.max(cur.needInput.answeredSeq ?? 0, owed?.answeredSeq ?? 0),
|
|
2480
|
+
},
|
|
2481
|
+
bumped: false,
|
|
2482
|
+
snapshotBlind: cur.needInput.cutoverToken === opts.armToken,
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
catch (readErr) {
|
|
2487
|
+
console.warn(`[AgentManager] needInput watermark read-back failed (agent=${agentId}):`, readErr);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
return { wm: null, bumped: false, snapshotBlind: true };
|
|
2491
|
+
}
|
|
2492
|
+
return { wm, bumped: wm !== null, snapshotBlind: wm === null ? true : snapshotBlind };
|
|
2493
|
+
}
|
|
2494
|
+
// Single entry for arming phase-signal watches — every path must flow through the
|
|
2495
|
+
// epoch bump, or stale-generation commits could survive the re-arm.
|
|
2496
|
+
async startPhaseSignalWatch(args) {
|
|
2175
2497
|
if (!this.phaseSignalWatcher)
|
|
2176
|
-
return
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2498
|
+
return true;
|
|
2499
|
+
const { needInputMode, needInputPaneCutover: _paneCutover, ...rest } = args;
|
|
2500
|
+
const mode = needInputMode ?? 'restore';
|
|
2501
|
+
const watcher = this.phaseSignalWatcher;
|
|
2502
|
+
// Claim ownership before bumping: a rejected arm must not bump (it would fence the
|
|
2503
|
+
// owner), and the claim keeps this arm visible to later probes while it subscribes.
|
|
2504
|
+
const gateResult = await this.withNeedInputArmLock(args.agentId, args.taskId, async () => {
|
|
2505
|
+
const claim = watcher.claimArm?.({
|
|
2506
|
+
taskId: args.taskId,
|
|
2507
|
+
agentId: args.agentId,
|
|
2508
|
+
token: args.token,
|
|
2509
|
+
...(args.onlyReplaceOwnToken !== undefined ? { onlyReplaceOwnToken: args.onlyReplaceOwnToken } : {}),
|
|
2510
|
+
...(args.replaceScope !== undefined ? { replaceScope: args.replaceScope } : {}),
|
|
2511
|
+
});
|
|
2512
|
+
if (claim === null)
|
|
2513
|
+
return 'rejected';
|
|
2514
|
+
const bump = await this.bumpNeedInputEpochForArm(args.agentId, args.taskId, mode, {
|
|
2515
|
+
armToken: args.token,
|
|
2516
|
+
...(args.needInputPaneCutover ? { paneCutover: true } : {}),
|
|
2517
|
+
});
|
|
2518
|
+
// Only a real generation advance may drop the queue: same-generation intents
|
|
2519
|
+
// (bump failed, watermark read back) are still owed to the store.
|
|
2520
|
+
if (bump.bumped)
|
|
2521
|
+
this.clearNeedInputRetryFor(args.agentId, args.taskId);
|
|
2522
|
+
return { ...bump, claim };
|
|
2182
2523
|
});
|
|
2524
|
+
if (gateResult === 'rejected')
|
|
2525
|
+
return false;
|
|
2526
|
+
const { wm, bumped, snapshotBlind, claim } = gateResult;
|
|
2527
|
+
// Same synchronous section as the bump result: old-generation answers must not slip
|
|
2528
|
+
// between the bump and the handoff (start() migrates the predecessor synchronously).
|
|
2529
|
+
if (wm !== null && bumped && mode === 'restore') {
|
|
2530
|
+
const delta = this.collectPredecessorNeedInputDelta(args.agentId, args.taskId, wm.epoch);
|
|
2531
|
+
if (delta) {
|
|
2532
|
+
void this.commitNeedInputWatermark({
|
|
2533
|
+
agentId: args.agentId,
|
|
2534
|
+
taskId: args.taskId,
|
|
2535
|
+
epoch: wm.epoch,
|
|
2536
|
+
askSeq: Math.max(delta.askSeq, wm.askSeq),
|
|
2537
|
+
answeredSeq: Math.max(delta.answeredSeq, wm.answeredSeq),
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
// wm === null (no binding / store error): arm anyway with badge commits disabled —
|
|
2542
|
+
// the badge is display-only, the signal watch drives task progress.
|
|
2543
|
+
try {
|
|
2544
|
+
return await watcher.start({
|
|
2545
|
+
...rest,
|
|
2546
|
+
...(claim != null ? { armClaimId: claim } : {}),
|
|
2547
|
+
// Inherit is about restore semantics, not about having a generation: a degraded
|
|
2548
|
+
// restore still has to keep the predecessor's pending question in memory.
|
|
2549
|
+
needInputInherit: mode === 'restore',
|
|
2550
|
+
...(snapshotBlind ? { needInputSnapshotBlind: true } : {}),
|
|
2551
|
+
...(wm !== null ? { needInput: wm } : {}),
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
finally {
|
|
2555
|
+
watcher.releaseArm?.(claim);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
// Web-fallback confirm: settles the newest known question (store ∪ retry queue) as
|
|
2559
|
+
// answered in ONE store update — usable when no watch entry exists (pre-recovery,
|
|
2560
|
+
// arm failure, post-stop). Queue contributions are snapshotted synchronously; the
|
|
2561
|
+
// updater derives everything else from `existing`, so no nested store calls.
|
|
2562
|
+
async confirmNeedInputAnswered(agentId) {
|
|
2563
|
+
const queueAsk = new Map();
|
|
2564
|
+
for (const [key, wm] of this.needInputRetry) {
|
|
2565
|
+
const parsed = this.parseNeedInputRetryKey(key);
|
|
2566
|
+
if (!parsed || parsed.agentId !== agentId)
|
|
2567
|
+
continue;
|
|
2568
|
+
const mapKey = `${parsed.taskId} ${parsed.epoch}`;
|
|
2569
|
+
queueAsk.set(mapKey, Math.max(queueAsk.get(mapKey) ?? 0, wm.askSeq));
|
|
2570
|
+
}
|
|
2571
|
+
for (const intent of this.needInputWritesInFlight.values()) {
|
|
2572
|
+
if (intent.agentId !== agentId)
|
|
2573
|
+
continue;
|
|
2574
|
+
const mapKey = `${intent.taskId} ${intent.epoch}`;
|
|
2575
|
+
queueAsk.set(mapKey, Math.max(queueAsk.get(mapKey) ?? 0, intent.askSeq));
|
|
2576
|
+
}
|
|
2577
|
+
let settled = null;
|
|
2578
|
+
try {
|
|
2579
|
+
await this.agentStore.update(agentId, (existing) => {
|
|
2580
|
+
if (!existing?.taskId)
|
|
2581
|
+
return AGENT_STORE_NOOP;
|
|
2582
|
+
const cur = existing.needInput;
|
|
2583
|
+
const epoch = cur?.epoch ?? 0;
|
|
2584
|
+
const qAsk = queueAsk.get(`${existing.taskId} ${epoch}`) ?? 0;
|
|
2585
|
+
const askSeq = Math.max(cur?.askSeq ?? 0, qAsk);
|
|
2586
|
+
const answeredSeq = Math.max(cur?.answeredSeq ?? 0, askSeq);
|
|
2587
|
+
settled = { taskId: existing.taskId, epoch, askSeq };
|
|
2588
|
+
if (cur && (cur.askSeq ?? 0) === askSeq && (cur.answeredSeq ?? 0) === answeredSeq && cur.at === undefined) {
|
|
2589
|
+
return AGENT_STORE_NOOP;
|
|
2590
|
+
}
|
|
2591
|
+
if (!cur && askSeq === 0)
|
|
2592
|
+
return AGENT_STORE_NOOP;
|
|
2593
|
+
return {
|
|
2594
|
+
...existing,
|
|
2595
|
+
needInput: {
|
|
2596
|
+
epoch, askSeq, answeredSeq,
|
|
2597
|
+
...(cur?.cutoverToken !== undefined ? { cutoverToken: cur.cutoverToken } : {}),
|
|
2598
|
+
},
|
|
2599
|
+
updatedAt: new Date().toISOString(),
|
|
2600
|
+
};
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
catch (err) {
|
|
2604
|
+
console.warn(`[AgentManager] confirmNeedInputAnswered(${agentId}) failed:`, err);
|
|
2605
|
+
if (settled !== null) {
|
|
2606
|
+
const s = settled;
|
|
2607
|
+
this.enqueueNeedInputRetry(this.needInputRetryKey(agentId, s.taskId, s.epoch), s.askSeq, s.askSeq);
|
|
2608
|
+
}
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
if (settled !== null) {
|
|
2612
|
+
const s = settled;
|
|
2613
|
+
this.settleNeedInputRetry(this.needInputRetryKey(agentId, s.taskId, s.epoch), s.askSeq, s.askSeq, 'ok');
|
|
2614
|
+
}
|
|
2183
2615
|
}
|
|
2184
|
-
// The user typed into the agent's terminal — its
|
|
2616
|
+
// The user typed into the agent's terminal — its open question counts as answered.
|
|
2617
|
+
// The store fallback runs only when no watch entry exists (pre-recovery, arm failure,
|
|
2618
|
+
// post-stop): with a live entry it would race the entry's in-flight writes and could
|
|
2619
|
+
// confirm an ask that arrived after this input.
|
|
2185
2620
|
async notifyHumanTerminalInput(agentId) {
|
|
2186
|
-
|
|
2187
|
-
|
|
2621
|
+
// Rearm first: it settles entry state synchronously, so a question printed during
|
|
2622
|
+
// this call cannot be confirmed by mistake. The binding read only decides whether a
|
|
2623
|
+
// watcher of the CURRENT task took the input — a leftover entry of a finished task
|
|
2624
|
+
// must not suppress the store fallback.
|
|
2625
|
+
const handled = await this.phaseSignalWatcher?.rearmNeedInput(agentId);
|
|
2626
|
+
let taskId;
|
|
2627
|
+
try {
|
|
2628
|
+
taskId = (await this.agentStore.get(agentId))?.taskId;
|
|
2629
|
+
}
|
|
2630
|
+
catch (err) {
|
|
2631
|
+
console.warn(`[AgentManager] notifyHumanTerminalInput(${agentId}) binding read failed:`, err);
|
|
2632
|
+
}
|
|
2633
|
+
if (taskId === undefined || !handled?.has(taskId))
|
|
2634
|
+
await this.confirmNeedInputAnswered(agentId);
|
|
2188
2635
|
}
|
|
2189
2636
|
async clearAwaitingHuman(agentId, opts = {}) {
|
|
2190
2637
|
const now = new Date().toISOString();
|
|
@@ -2445,6 +2892,8 @@ export class AgentManager {
|
|
|
2445
2892
|
next.lockToken = existing.lockToken;
|
|
2446
2893
|
if (existing.startedAt !== undefined)
|
|
2447
2894
|
next.startedAt = existing.startedAt;
|
|
2895
|
+
if (existing.needInput !== undefined)
|
|
2896
|
+
next.needInput = existing.needInput;
|
|
2448
2897
|
}
|
|
2449
2898
|
return next;
|
|
2450
2899
|
});
|
|
@@ -3073,6 +3522,8 @@ export class AgentManager {
|
|
|
3073
3522
|
const armed = await this.setupPhaseSignalWatcher(taskId, agentId, expectedKinds, token, {
|
|
3074
3523
|
skipSnapshot: true,
|
|
3075
3524
|
onlyReplaceOwnToken: true,
|
|
3525
|
+
needInputMode: 'fresh',
|
|
3526
|
+
needInputPaneCutover: true,
|
|
3076
3527
|
// git 双 watcher 下 fence 只应比较自己的 (taskId, agentId) 条目:task 域会拿
|
|
3077
3528
|
// sibling(reconciliation)的不同 token 否决本应合法的重布防
|
|
3078
3529
|
...(task.reviewMode === 'git' ? { replaceScope: 'agent' } : {}),
|
|
@@ -3614,6 +4065,7 @@ export class AgentManager {
|
|
|
3614
4065
|
agentId: task.agentId,
|
|
3615
4066
|
expectedKinds: 'pr-merge-ready',
|
|
3616
4067
|
token: value.token,
|
|
4068
|
+
needInputMode: 'fresh',
|
|
3617
4069
|
});
|
|
3618
4070
|
if (!armed) {
|
|
3619
4071
|
// completion 没有 consumer 就不算安装成功:撤销本次写,durable pending(若有)随原状保留
|
|
@@ -3915,6 +4367,30 @@ export class AgentManager {
|
|
|
3915
4367
|
}
|
|
3916
4368
|
// 配置身份切换与任务创建共栅栏(TOCTOU):blocker 扫描与提交在 withTaskLock 内执行,
|
|
3917
4369
|
// createTask 的「读配置快照+落任务」同经该锁,扫描时不可见的并发新任务不存在。
|
|
4370
|
+
// 启动期按「未引用」跳过 skill 池的插件,被热配置首次引用时同步重扫;仍不可物化即拒绝提交——
|
|
4371
|
+
// 可解析但物化为空的 tool 会让派发要求加载一个不存在的 skill。
|
|
4372
|
+
async ensurePluginSkillPools(next) {
|
|
4373
|
+
if (!this.pluginRegistry)
|
|
4374
|
+
return;
|
|
4375
|
+
for (const project of next.project) {
|
|
4376
|
+
if (projectReviewMode(next, project) !== 'git')
|
|
4377
|
+
continue;
|
|
4378
|
+
const tool = resolveProjectTool(project);
|
|
4379
|
+
if (tool === undefined)
|
|
4380
|
+
continue;
|
|
4381
|
+
const plugin = this.pluginRegistry.resolveTool(tool);
|
|
4382
|
+
// 静默放行会存下一个「无 driver、无平台 skill、重启即 fatal」的配置(启动期同判据)
|
|
4383
|
+
if (plugin === undefined) {
|
|
4384
|
+
throw new Error(`project '${project.id}': no git-driver plugin provides tool '${tool}' — install it under ~/.baxian/plugins/ first`);
|
|
4385
|
+
}
|
|
4386
|
+
if (this.skillRegistry.pluginSkillNames({ pluginTools: [tool] }).length > 0)
|
|
4387
|
+
continue;
|
|
4388
|
+
await this.skillRegistry.scanPluginSkills(tool, join(plugin.pluginPath, 'skills'));
|
|
4389
|
+
if (this.skillRegistry.pluginSkillNames({ pluginTools: [tool] }).length === 0) {
|
|
4390
|
+
throw new Error(`plugin '${plugin.manifest.name}' provides no materializable skills for tool '${tool}'`);
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
3918
4394
|
async guardGitConfigCommit(current, next, scan, commit) {
|
|
3919
4395
|
return this.withTaskLock(async () => {
|
|
3920
4396
|
const blockers = await scan(this, current, next);
|
|
@@ -4336,6 +4812,82 @@ export class AgentManager {
|
|
|
4336
4812
|
return {};
|
|
4337
4813
|
return { platformBinding: { mode: 'git', repoKey: repoIdentityKey(project.repo), tool } };
|
|
4338
4814
|
}
|
|
4815
|
+
// agent 面一律 PATH 解析 tool;server 面 binary/env 不进入该轨。
|
|
4816
|
+
agentGitPreflightContext(projectId) {
|
|
4817
|
+
if (this.effectiveReviewMode(projectId) !== 'git')
|
|
4818
|
+
return undefined;
|
|
4819
|
+
const project = this.getProjectConfig(projectId);
|
|
4820
|
+
if (!project || !this.pluginRegistry)
|
|
4821
|
+
return undefined;
|
|
4822
|
+
const tool = resolveProjectTool(project);
|
|
4823
|
+
if (tool === undefined)
|
|
4824
|
+
return undefined;
|
|
4825
|
+
const plugin = this.pluginRegistry.resolveTool(tool);
|
|
4826
|
+
if (plugin === undefined)
|
|
4827
|
+
return undefined;
|
|
4828
|
+
const renderCtx = buildDriverRunContext(project.repo, tool);
|
|
4829
|
+
return {
|
|
4830
|
+
tool,
|
|
4831
|
+
minToolVersion: plugin.manifest.minToolVersion,
|
|
4832
|
+
steps: plugin.spec.preflight,
|
|
4833
|
+
agentCommands: plugin.spec.agentCommands,
|
|
4834
|
+
renderCtx,
|
|
4835
|
+
driverFor: (exec) => new GitDriver(plugin, renderCtx, exec),
|
|
4836
|
+
};
|
|
4837
|
+
}
|
|
4838
|
+
// cli 字段族每次派发实时渲染,不做创建时快照。渲染前按持久化 binding 校验身份——
|
|
4839
|
+
// 离线漂移的配置会把旧任务的 PR 号/分支/令牌带到新仓库,与平台操作同一守卫。
|
|
4840
|
+
platformCliContextOf(task) {
|
|
4841
|
+
if (task.reviewMode !== 'git')
|
|
4842
|
+
return undefined;
|
|
4843
|
+
this.assertPlatformBinding(task);
|
|
4844
|
+
const project = this.getProjectConfig(task.projectId);
|
|
4845
|
+
if (!project)
|
|
4846
|
+
return undefined;
|
|
4847
|
+
const tool = resolveProjectTool(project);
|
|
4848
|
+
if (tool === undefined)
|
|
4849
|
+
return undefined;
|
|
4850
|
+
const ctx = buildDriverRunContext(project.repo, tool);
|
|
4851
|
+
return {
|
|
4852
|
+
tool,
|
|
4853
|
+
host: ctx.host,
|
|
4854
|
+
repo: ctx.repoPath,
|
|
4855
|
+
repoEncoded: encodeURIComponent(ctx.repoPath),
|
|
4856
|
+
...(project.gitCli?.notes !== undefined ? { notes: project.gitCli.notes } : {}),
|
|
4857
|
+
};
|
|
4858
|
+
}
|
|
4859
|
+
// base 快照一经持久化不可变;无快照保持缺省,由 agent 现场显式查询。
|
|
4860
|
+
async ensureGitBaseSnapshot(task, phase) {
|
|
4861
|
+
if (task.reviewMode !== 'git' || (phase !== 'develop' && phase !== 'code'))
|
|
4862
|
+
return task;
|
|
4863
|
+
if (task.baseBranch !== undefined)
|
|
4864
|
+
return task;
|
|
4865
|
+
// 读默认分支与落快照都以 binding 为前提:失配任务先写入新仓库的 base 会永久污染不可变快照
|
|
4866
|
+
this.assertPlatformBinding(task);
|
|
4867
|
+
const base = this.platformDefaultBranchOf?.(task.projectId);
|
|
4868
|
+
if (base === undefined)
|
|
4869
|
+
return task;
|
|
4870
|
+
return this.withTaskLock(async () => {
|
|
4871
|
+
const fresh = await this.taskStore.get(task.id);
|
|
4872
|
+
if (!fresh)
|
|
4873
|
+
return task;
|
|
4874
|
+
if (fresh.baseBranch !== undefined)
|
|
4875
|
+
return fresh;
|
|
4876
|
+
this.assertPlatformBinding(fresh);
|
|
4877
|
+
const next = { ...fresh, baseBranch: base, updatedAt: new Date().toISOString() };
|
|
4878
|
+
await this.taskStore.set(next);
|
|
4879
|
+
return next;
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
async noteReviewConversationRevision(taskId) {
|
|
4883
|
+
await this.withTaskLock(async () => {
|
|
4884
|
+
const task = await this.taskStore.get(taskId);
|
|
4885
|
+
if (!task || TERMINAL_STATUSES.includes(task.status))
|
|
4886
|
+
return;
|
|
4887
|
+
const now = new Date().toISOString();
|
|
4888
|
+
await this.taskStore.set({ ...task, reviewConversationUpdatedAt: now, updatedAt: now });
|
|
4889
|
+
});
|
|
4890
|
+
}
|
|
4339
4891
|
// 指纹为纯身份三元组(spec §4):binary/env/skill 文本 live 解析可演进,身份失配即拒绝
|
|
4340
4892
|
assertPlatformBinding(task) {
|
|
4341
4893
|
const binding = task.platformBinding;
|
|
@@ -4672,7 +5224,10 @@ export class AgentManager {
|
|
|
4672
5224
|
if (this.repoStoreFactory) {
|
|
4673
5225
|
return this.repoStoreFactory(runner, project.repo, agent.mode, host, this.repoCache, agent.id, agent.workdir);
|
|
4674
5226
|
}
|
|
4675
|
-
|
|
5227
|
+
const cloneViaGh = this.effectiveReviewMode(project.id) === 'git'
|
|
5228
|
+
? resolveProjectTool(project) === 'gh'
|
|
5229
|
+
: undefined;
|
|
5230
|
+
return new RepoStore(runner, project.repo, agent.mode, host, this.repoCache, agent.id, agent.workdir, cloneViaGh);
|
|
4676
5231
|
}
|
|
4677
5232
|
async ensureWorkdir(agent, project, runner) {
|
|
4678
5233
|
const repoStore = this.createRepoStore(agent, project, runner);
|
|
@@ -4915,6 +5470,8 @@ export class AgentManager {
|
|
|
4915
5470
|
...(existing?.paneId !== undefined ? { paneId: existing.paneId } : {}),
|
|
4916
5471
|
...(existing?.workdir !== undefined ? { workdir: existing.workdir } : {}),
|
|
4917
5472
|
...(existing?.creationToken !== undefined ? { creationToken: existing.creationToken } : {}),
|
|
5473
|
+
// A new task never inherits the previous one's question: void the watermark.
|
|
5474
|
+
needInput: { epoch: (existing?.needInput?.epoch ?? 0) + 1 },
|
|
4918
5475
|
};
|
|
4919
5476
|
});
|
|
4920
5477
|
}
|
|
@@ -5432,6 +5989,8 @@ export class AgentManager {
|
|
|
5432
5989
|
...(existing?.paneId !== undefined ? { paneId: existing.paneId } : {}),
|
|
5433
5990
|
...(existing?.workdir !== undefined ? { workdir: existing.workdir } : {}),
|
|
5434
5991
|
...(existing?.creationToken !== undefined ? { creationToken: existing.creationToken } : {}),
|
|
5992
|
+
// A new task never inherits the previous one's question: void the watermark.
|
|
5993
|
+
needInput: { epoch: (existing?.needInput?.epoch ?? 0) + 1 },
|
|
5435
5994
|
};
|
|
5436
5995
|
});
|
|
5437
5996
|
}
|
|
@@ -5757,9 +6316,9 @@ export class AgentManager {
|
|
|
5757
6316
|
await this.agentStore.update(agentId, (stateNow) => {
|
|
5758
6317
|
if (!stateNow || stateNow.taskId !== taskId)
|
|
5759
6318
|
return AGENT_STORE_NOOP;
|
|
5760
|
-
|
|
6319
|
+
// Watermark stays: the arm-time epoch bump owns superseding the old question.
|
|
5761
6320
|
return {
|
|
5762
|
-
...
|
|
6321
|
+
...stateNow,
|
|
5763
6322
|
paneId,
|
|
5764
6323
|
workdir,
|
|
5765
6324
|
updatedAt: new Date().toISOString(),
|
|
@@ -5792,13 +6351,16 @@ export class AgentManager {
|
|
|
5792
6351
|
...(opts.serverPriorResponse ? { serverPriorResponse: opts.serverPriorResponse } : {}),
|
|
5793
6352
|
});
|
|
5794
6353
|
}
|
|
6354
|
+
const taskForPrompt = await this.ensureGitBaseSnapshot(task, phase);
|
|
6355
|
+
const platformCli = this.platformCliContextOf(taskForPrompt);
|
|
5795
6356
|
prompt = buildPromptInline({
|
|
5796
|
-
task,
|
|
6357
|
+
task: taskForPrompt,
|
|
5797
6358
|
phase,
|
|
5798
6359
|
agent,
|
|
5799
6360
|
workdir: dispatchWorkdir,
|
|
5800
6361
|
skillRegistry: this.skillRegistry,
|
|
5801
6362
|
hasQaPartner,
|
|
6363
|
+
...(platformCli ? { platformCli } : {}),
|
|
5802
6364
|
...(promptSignalToken ? { signalToken: promptSignalToken } : {}),
|
|
5803
6365
|
...(promptSpecRound !== undefined ? { currentSpecRound: promptSpecRound } : {}),
|
|
5804
6366
|
...(imagePaths.length ? { imagePaths } : {}),
|
|
@@ -6368,13 +6930,16 @@ export class AgentManager {
|
|
|
6368
6930
|
...(opts.serverPriorResponse ? { serverPriorResponse: opts.serverPriorResponse } : {}),
|
|
6369
6931
|
});
|
|
6370
6932
|
}
|
|
6933
|
+
const taskForPrompt = await this.ensureGitBaseSnapshot(task, phase);
|
|
6934
|
+
const platformCli = this.platformCliContextOf(taskForPrompt);
|
|
6371
6935
|
prompt = buildPromptInline({
|
|
6372
|
-
task,
|
|
6936
|
+
task: taskForPrompt,
|
|
6373
6937
|
phase,
|
|
6374
6938
|
agent,
|
|
6375
6939
|
workdir: verifiedWorkdir,
|
|
6376
6940
|
skillRegistry: this.skillRegistry,
|
|
6377
6941
|
hasQaPartner: task.qaAgentId !== undefined,
|
|
6942
|
+
...(platformCli ? { platformCli } : {}),
|
|
6378
6943
|
...(signalToken ? { signalToken } : {}),
|
|
6379
6944
|
...(useIncrementalNudge
|
|
6380
6945
|
? { postApproveRedispatchCount: opts.postApproveRedispatchCount }
|
|
@@ -6460,10 +7025,11 @@ export class AgentManager {
|
|
|
6460
7025
|
|| latest.workdir !== verifiedWorkdir) {
|
|
6461
7026
|
return AGENT_STORE_NOOP;
|
|
6462
7027
|
}
|
|
6463
|
-
// The continuation prompt supersedes
|
|
6464
|
-
|
|
7028
|
+
// The continuation prompt supersedes the previous question, but the arm-time
|
|
7029
|
+
// epoch bump owns that transition — stripping here would race an arm that
|
|
7030
|
+
// already re-established the watermark (post-approve arms before injecting).
|
|
6465
7031
|
return {
|
|
6466
|
-
...
|
|
7032
|
+
...latest,
|
|
6467
7033
|
paneId,
|
|
6468
7034
|
workdir: verifiedWorkdir,
|
|
6469
7035
|
lockToken,
|
|
@@ -6766,6 +7332,7 @@ export class AgentManager {
|
|
|
6766
7332
|
...(latest.taskId !== undefined ? { taskId: latest.taskId } : {}),
|
|
6767
7333
|
...(latest.lockToken !== undefined ? { lockToken: latest.lockToken } : {}),
|
|
6768
7334
|
...(latest.startedAt !== undefined ? { startedAt: latest.startedAt } : {}),
|
|
7335
|
+
...(latest.needInput !== undefined ? { needInput: latest.needInput } : {}),
|
|
6769
7336
|
};
|
|
6770
7337
|
if (!preserveHeld)
|
|
6771
7338
|
return withBinding;
|
|
@@ -7360,7 +7927,10 @@ export class AgentManager {
|
|
|
7360
7927
|
}
|
|
7361
7928
|
opts.onPassArmed?.(armedToken);
|
|
7362
7929
|
const dispatchTaskAfterClaim = await this.taskStore.get(taskId);
|
|
7363
|
-
const armed = await this.setupPhaseSignalWatcher(taskId, qaId, ['pr-approved', 'pr-changes-requested'], armedToken,
|
|
7930
|
+
const armed = await this.setupPhaseSignalWatcher(taskId, qaId, ['pr-approved', 'pr-changes-requested'], armedToken, {
|
|
7931
|
+
needInputMode: 'fresh',
|
|
7932
|
+
...(dispatchTaskAfterClaim?.reviewMode === 'git' ? { replaceScope: 'agent' } : {}),
|
|
7933
|
+
});
|
|
7364
7934
|
if (armed && dispatchTaskAfterClaim?.reviewMode === 'git') {
|
|
7365
7935
|
await this.rearmGitReconciliationWatcher(taskId);
|
|
7366
7936
|
}
|
|
@@ -8322,6 +8892,8 @@ export class AgentManager {
|
|
|
8322
8892
|
...(opts.onlyReplaceOwnToken ? { onlyReplaceOwnToken: true } : {}),
|
|
8323
8893
|
...(opts.replaceScope ? { replaceScope: opts.replaceScope } : {}),
|
|
8324
8894
|
...(opts.onReadFile ? { onReadFile: opts.onReadFile } : {}),
|
|
8895
|
+
...(opts.needInputMode ? { needInputMode: opts.needInputMode } : {}),
|
|
8896
|
+
...(opts.needInputPaneCutover ? { needInputPaneCutover: true } : {}),
|
|
8325
8897
|
});
|
|
8326
8898
|
}
|
|
8327
8899
|
catch (err) {
|
|
@@ -8332,6 +8904,10 @@ export class AgentManager {
|
|
|
8332
8904
|
async armPostDispatchSignalOrHold(taskId, agentId, expectedKinds, token, skipSnapshot = false, onReadFile) {
|
|
8333
8905
|
const armed = await this.setupPhaseSignalWatcher(taskId, agentId, expectedKinds, token, {
|
|
8334
8906
|
skipSnapshot,
|
|
8907
|
+
// Every post-dispatch arm follows a freshly injected prompt: the previous
|
|
8908
|
+
// question is superseded, its watermark must not carry over (fix/publish
|
|
8909
|
+
// continuations included).
|
|
8910
|
+
needInputMode: 'fresh',
|
|
8335
8911
|
...(onReadFile ? { onReadFile } : {}),
|
|
8336
8912
|
});
|
|
8337
8913
|
if (!armed)
|
|
@@ -8372,7 +8948,10 @@ export class AgentManager {
|
|
|
8372
8948
|
// pair 的重铸已随「进入 review 的 transition」单写落盘(spec §7 与新轮可见性原子);
|
|
8373
8949
|
// 这里只轮换 signal token 并布防
|
|
8374
8950
|
await this.updateTask(taskId, { signalToken: newToken });
|
|
8375
|
-
const armed = await this.setupPhaseSignalWatcher(taskId, agentId, expectedKinds, newToken,
|
|
8951
|
+
const armed = await this.setupPhaseSignalWatcher(taskId, agentId, expectedKinds, newToken, {
|
|
8952
|
+
needInputMode: 'fresh',
|
|
8953
|
+
...(isGit ? { replaceScope: 'agent' } : {}),
|
|
8954
|
+
});
|
|
8376
8955
|
if (armed && isGit && kinds.includes('pr-approved'))
|
|
8377
8956
|
await this.rearmGitReconciliationWatcher(taskId);
|
|
8378
8957
|
return { token: newToken, armed };
|
|
@@ -8782,7 +9361,7 @@ export class AgentManager {
|
|
|
8782
9361
|
signalToken,
|
|
8783
9362
|
specDocuments,
|
|
8784
9363
|
...(currentSpecRound !== undefined ? { currentSpecRound } : {}),
|
|
8785
|
-
armBeforeInject: () => this.setupPhaseSignalWatcher(task.id, devAgentId, expectedKind, signalToken),
|
|
9364
|
+
armBeforeInject: () => this.setupPhaseSignalWatcher(task.id, devAgentId, expectedKind, signalToken, { needInputMode: 'fresh' }),
|
|
8786
9365
|
};
|
|
8787
9366
|
return this.isResearchHandoff(task, devAgentId)
|
|
8788
9367
|
? this.startSession(task.id, devAgentId, 'code', {
|
|
@@ -9046,6 +9625,7 @@ export class AgentManager {
|
|
|
9046
9625
|
dispatchedCheckoutMode = ctx.serverReviewCheckout;
|
|
9047
9626
|
const allowReadFile = opts.phase === 'spec' || opts.continuation || ctx.serverReviewCheckout === 'base';
|
|
9048
9627
|
return this.setupPhaseSignalWatcher(taskId, qaId, expectedKind, newToken, {
|
|
9628
|
+
needInputMode: 'fresh',
|
|
9049
9629
|
...(allowReadFile
|
|
9050
9630
|
? { onReadFile: (req) => { void this.handleReadFileRequest(taskId, qaId, req); } }
|
|
9051
9631
|
: {}),
|