@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -2
- package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
- package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
- package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
- package/dist/resources/extensions/gsd/commands-context.js +19 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
- package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
- package/dist/resources/extensions/gsd/db/queries.js +60 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
- package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
- package/dist/resources/extensions/gsd/forensics.js +2 -32
- package/dist/resources/extensions/gsd/git-service.js +4 -4
- package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
- package/dist/resources/extensions/gsd/health-widget.js +55 -29
- package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
- package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
- package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
- package/dist/resources/extensions/gsd/quick.js +45 -2
- package/dist/resources/extensions/gsd/session-forensics.js +11 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
- package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
- package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
- package/dist/resources/extensions/gsd/unit-registry.js +25 -3
- package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
- package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
- package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
- package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/workflow.d.ts +1 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +2 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/README.md +1 -1
- package/packages/mcp-server/dist/server.d.ts +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +3 -3
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +34 -20
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +20 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
- package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
- package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
- package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
- package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
- package/src/resources/extensions/gsd/commands-context.ts +18 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
- package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
- package/src/resources/extensions/gsd/db/queries.ts +79 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
- package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
- package/src/resources/extensions/gsd/forensics.ts +2 -33
- package/src/resources/extensions/gsd/git-service.ts +5 -5
- package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
- package/src/resources/extensions/gsd/health-widget.ts +69 -32
- package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
- package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
- package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
- package/src/resources/extensions/gsd/quick.ts +43 -2
- package/src/resources/extensions/gsd/session-forensics.ts +11 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
- package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
- package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
- package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
- package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
- package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
- package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
- package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
- package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
- package/src/resources/extensions/gsd/unit-registry.ts +25 -3
- package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
- package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
- package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
|
@@ -24,7 +24,7 @@ import { isContextModeEnabled } from "./preferences-types.js";
|
|
|
24
24
|
import { parseRoadmap } from "./parsers-legacy.js";
|
|
25
25
|
import type { GSDState, InlineLevel } from "./types.js";
|
|
26
26
|
import type { GSDPreferences } from "./preferences.js";
|
|
27
|
-
import { join, basename } from "node:path";
|
|
27
|
+
import { join, basename, relative } from "node:path";
|
|
28
28
|
import { existsSync } from "node:fs";
|
|
29
29
|
import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary, type MinimalModelRegistry } from "./context-budget.js";
|
|
30
30
|
import { getPendingGates, getPendingGatesForTurn } from "./gsd-db.js";
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
type ExcerptResolver,
|
|
49
49
|
} from "./unit-context-composer.js";
|
|
50
50
|
import { resolveManifest, type ArtifactKey } from "./unit-context-manifest.js";
|
|
51
|
+
import { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
|
|
51
52
|
import { compileUnitContextContract, type UnitPromptContextContract } from "./tool-contract.js";
|
|
52
53
|
import { readCompactionSnapshot } from "./compaction-snapshot.js";
|
|
53
54
|
import { logWarning } from "./workflow-logger.js";
|
|
@@ -323,19 +324,53 @@ function requireComposedArtifactBlock(
|
|
|
323
324
|
return block.body;
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
interface ExecuteTaskOnDemandResult {
|
|
328
|
+
/** Rendered block for the prompt; empty string when not shown. */
|
|
329
|
+
text: string;
|
|
330
|
+
/**
|
|
331
|
+
* Telemetry skip reason when the block was suppressed; null when the block
|
|
332
|
+
* is included. Callers pass this directly to `trackPromptContext`.
|
|
333
|
+
*/
|
|
334
|
+
skipReason: string | null;
|
|
335
|
+
}
|
|
336
|
+
|
|
326
337
|
function renderExecuteTaskOnDemandContext(
|
|
327
338
|
base: string,
|
|
328
339
|
mid: string,
|
|
329
340
|
sid: string,
|
|
330
341
|
artifacts: readonly ArtifactKey[],
|
|
331
|
-
):
|
|
332
|
-
if (!artifacts.includes("slice-research"))
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
342
|
+
): ExecuteTaskOnDemandResult {
|
|
343
|
+
if (!artifacts.includes("slice-research")) {
|
|
344
|
+
return { text: "", skipReason: "not declared by contract" };
|
|
345
|
+
}
|
|
346
|
+
// Mirror dispatch's dual-resolution logic: worktree projection path first,
|
|
347
|
+
// then the authoritative project-root path via resolveExpectedArtifactPath.
|
|
348
|
+
// In worktree layouts the RESEARCH file may live under the project-root .gsd
|
|
349
|
+
// and not have been copied into the worktree projection, so resolveSliceFile
|
|
350
|
+
// (which looks only at gsdProjectionRoot) would miss it while dispatch would
|
|
351
|
+
// still treat research as satisfied.
|
|
352
|
+
const projectedFile = resolveSliceFile(base, mid, sid, "RESEARCH");
|
|
353
|
+
const researchFile: string | null = projectedFile ?? (() => {
|
|
354
|
+
const p = resolveExpectedArtifactPath("research-slice", `${mid}/${sid}`, base);
|
|
355
|
+
return p && existsSync(p) ? p : null;
|
|
356
|
+
})();
|
|
357
|
+
if (!researchFile) {
|
|
358
|
+
return { text: "", skipReason: "missing" };
|
|
359
|
+
}
|
|
360
|
+
// Use the layout-aware relative path when the file is in the worktree
|
|
361
|
+
// projection (relSliceFile), or fall back to node:path relative() when the
|
|
362
|
+
// file was only found via the project-root resolution path.
|
|
363
|
+
const researchPath = projectedFile
|
|
364
|
+
? relSliceFile(base, mid, sid, "RESEARCH")
|
|
365
|
+
: relative(base, researchFile);
|
|
366
|
+
return {
|
|
367
|
+
text: [
|
|
368
|
+
"## On-demand Context",
|
|
369
|
+
"",
|
|
370
|
+
`Slice research is available at \`${researchPath}\`. Read it only if the inlined task plan, slice plan excerpt, and carry-forward context do not explain a required implementation detail.`,
|
|
371
|
+
].join("\n"),
|
|
372
|
+
skipReason: null,
|
|
373
|
+
};
|
|
339
374
|
}
|
|
340
375
|
|
|
341
376
|
// ─── Executor Constraints ─────────────────────────────────────────────────────
|
|
@@ -1691,7 +1726,13 @@ export async function buildDiscussMilestonePrompt(
|
|
|
1691
1726
|
const draftContent = draftPath ? await loadFile(draftPath) : null;
|
|
1692
1727
|
|
|
1693
1728
|
if (includeDraftSeed && draftContent) {
|
|
1694
|
-
|
|
1729
|
+
const draftRelPath = relMilestoneFile(base, mid, "CONTEXT-DRAFT");
|
|
1730
|
+
const draftSeed = `### Prior Discussion Draft\nSource: \`${draftRelPath}\`\n\n${draftContent.trim()}`;
|
|
1731
|
+
const cappedDraftSeed = capPreamble(draftSeed);
|
|
1732
|
+
const truncationNote = cappedDraftSeed !== draftSeed
|
|
1733
|
+
? `\n\n_(Draft seed truncated; read the full draft at \`${draftRelPath}\` if needed.)_`
|
|
1734
|
+
: "";
|
|
1735
|
+
return `${promptWithContextMode}\n\n## Prior Discussion (Draft Seed)\n\nThe following draft was captured from a prior multi-milestone discussion. Use it as seed material — the user has already provided this context. Start with a brief reflection on what the draft covers, then probe for any gaps or open questions before writing the full CONTEXT.md.\n\n${cappedDraftSeed}${truncationNote}`;
|
|
1695
1736
|
}
|
|
1696
1737
|
|
|
1697
1738
|
return promptWithContextMode;
|
|
@@ -2807,13 +2848,14 @@ export async function buildExecuteTaskPrompt(
|
|
|
2807
2848
|
const slicePlanExcerpt = requireComposedArtifactBlock(contractedContext.blocks, "execute-task", "slice-plan");
|
|
2808
2849
|
const contractedCarryForward = requireComposedArtifactBlock(contractedContext.blocks, "execute-task", "prior-task-summaries");
|
|
2809
2850
|
const contractedTemplates = requireComposedArtifactBlock(contractedContext.blocks, "execute-task", "templates");
|
|
2810
|
-
const
|
|
2851
|
+
const onDemandResult = renderExecuteTaskOnDemandContext(base, mid, sid, contractedContext.onDemand);
|
|
2852
|
+
const onDemandContext = onDemandResult.text;
|
|
2811
2853
|
trackPromptContext(
|
|
2812
2854
|
contextTelemetry,
|
|
2813
2855
|
"slice-research",
|
|
2814
2856
|
onDemandContext ? "on-demand" : "skipped",
|
|
2815
2857
|
onDemandContext,
|
|
2816
|
-
onDemandContext ? undefined :
|
|
2858
|
+
onDemandContext ? undefined : onDemandResult.skipReason ?? undefined,
|
|
2817
2859
|
);
|
|
2818
2860
|
|
|
2819
2861
|
const prompt = loadPrompt("execute-task", {
|
|
@@ -537,14 +537,21 @@ export function verifyExpectedArtifact(
|
|
|
537
537
|
// discuss-milestone unit with CONTEXT at phases/NN-slug/ in the project root
|
|
538
538
|
// but not in the worktree resolves to null → "resolveExpectedArtifactPath
|
|
539
539
|
// returned null" → finalize-retry loop (#852).
|
|
540
|
-
|
|
540
|
+
//
|
|
541
|
+
// #870: the fallback must fire whenever the artifact is missing at
|
|
542
|
+
// `artifactBase`, NOT only when `artifactBase !== base`. When the real call
|
|
543
|
+
// site passes the worktree path as `base` (auto-post-unit.ts:1726 uses
|
|
544
|
+
// `s.currentUnit.workspaceRoot ?? s.basePath`), resolveCanonicalMilestoneRoot
|
|
545
|
+
// round-trips the worktree path back to itself, so `artifactBase === base`
|
|
546
|
+
// while still being a worktree path that lacks the projected artifact. The
|
|
547
|
+
// old `if (artifactBase !== base)` guard skipped the fallback in exactly that
|
|
548
|
+
// case, stranding CONTEXT at the project root and producing a stuck-loop.
|
|
549
|
+
if (!absPath || !existsSync(absPath)) {
|
|
541
550
|
const projectRoot = resolve(resolveWorktreeProjectRoot(artifactBase));
|
|
542
551
|
if (projectRoot && projectRoot !== artifactBase) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
absPath = projectPath;
|
|
547
|
-
}
|
|
552
|
+
const projectPath = resolveExpectedArtifactPath(unitType, unitId, projectRoot);
|
|
553
|
+
if (projectPath && existsSync(projectPath)) {
|
|
554
|
+
absPath = projectPath;
|
|
548
555
|
}
|
|
549
556
|
}
|
|
550
557
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// gsd-pi + src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts - Handles provider and agent-end recovery for GSD auto-mode.
|
|
2
2
|
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
|
|
4
|
+
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
4
6
|
|
|
5
7
|
import type { AgentEndEvent, ErrorContext } from "../auto/types.js";
|
|
6
8
|
import { logWarning } from "../workflow-logger.js";
|
|
@@ -11,7 +13,7 @@ import {
|
|
|
11
13
|
maybeHandleEmptyIntentTurn,
|
|
12
14
|
resetEmptyTurnCounter,
|
|
13
15
|
} from "../guided-flow.js";
|
|
14
|
-
import { clearPathCache } from "../paths.js";
|
|
16
|
+
import { clearPathCache, gsdRoot } from "../paths.js";
|
|
15
17
|
import {
|
|
16
18
|
getAutoDashboardData,
|
|
17
19
|
getAutoModeStartModel,
|
|
@@ -34,7 +36,7 @@ import { resolveModelId } from "../auto-model-selection.js";
|
|
|
34
36
|
import { resolveProjectRoot } from "../worktree.js";
|
|
35
37
|
import { clearDiscussionFlowState } from "./write-gate.js";
|
|
36
38
|
import { scheduleFallbackContinuation } from "./fallback-continuation.js";
|
|
37
|
-
import { clearGuidedUnitContext } from "../guided-unit-context.js";
|
|
39
|
+
import { clearGuidedUnitContext, getGuidedUnitContext, type GuidedUnitContext } from "../guided-unit-context.js";
|
|
38
40
|
import { resumeAutoAfterProviderDelay } from "./provider-error-resume.js";
|
|
39
41
|
import {
|
|
40
42
|
classifyError,
|
|
@@ -116,9 +118,14 @@ export function isUserInitiatedAbortMessage(message: string | undefined | null):
|
|
|
116
118
|
export function shouldDeferTransientErrorToCoreRetry(
|
|
117
119
|
cls: ErrorClass,
|
|
118
120
|
rawErrorMsg: string,
|
|
121
|
+
deferCheckMsg: string = rawErrorMsg,
|
|
119
122
|
): boolean {
|
|
120
123
|
if (!isTransient(cls) || cls.kind === "rate-limit") return false;
|
|
121
|
-
|
|
124
|
+
// Empty rawErrorMsg means the SDK terminated the session without providing an
|
|
125
|
+
// error string — core is done, not mid-retry. GSD must schedule its own
|
|
126
|
+
// retry rather than silently deferring to a core that has already exited.
|
|
127
|
+
if (!rawErrorMsg) return false;
|
|
128
|
+
return !/retry failed after \d+ attempts:/i.test(deferCheckMsg);
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
type ProviderModelFallbackParams = {
|
|
@@ -388,6 +395,108 @@ export function suppressTerminalDeletedWorktreeMessageEnd(event: MessageEndLike)
|
|
|
388
395
|
return true;
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
function modelLabel(ctx: ExtensionContext): string {
|
|
399
|
+
const provider = ctx.model?.provider;
|
|
400
|
+
const id = ctx.model?.id;
|
|
401
|
+
return provider && id ? `${provider}/${id}` : "unknown model";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isFatalManualGuidedTerminalFailure(lastMsg: unknown): boolean {
|
|
405
|
+
if (!isObjectRecord(lastMsg) || !("stopReason" in lastMsg)) return false;
|
|
406
|
+
if (lastMsg.stopReason === "error") {
|
|
407
|
+
const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
|
|
408
|
+
if (isUserInitiatedAbortMessage(rawErrorMsg)) return false;
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (lastMsg.stopReason !== "aborted") return false;
|
|
412
|
+
|
|
413
|
+
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
414
|
+
const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
|
|
415
|
+
return hasErrorMessage || !_hasEmptyAgentEndContent(content);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function terminalFailureDetail(lastMsg: unknown): string {
|
|
419
|
+
if (!isObjectRecord(lastMsg)) return "Provider turn ended with an unknown terminal error.";
|
|
420
|
+
const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
|
|
421
|
+
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
422
|
+
const displayMsg = resolveAgentEndErrorDisplay(rawErrorMsg, content).replace(/\s+/g, " ").trim();
|
|
423
|
+
if (displayMsg) return displayMsg.length > 300 ? `${displayMsg.slice(0, 300)}...` : displayMsg;
|
|
424
|
+
return lastMsg.stopReason === "aborted"
|
|
425
|
+
? "Provider turn aborted with error context."
|
|
426
|
+
: "Provider stream ended with stopReason=error.";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function nextManualActivitySequence(activityDir: string): string {
|
|
430
|
+
let maxSeq = 0;
|
|
431
|
+
try {
|
|
432
|
+
for (const file of readdirSync(activityDir)) {
|
|
433
|
+
const match = /^(\d+)-/.exec(file);
|
|
434
|
+
if (match) maxSeq = Math.max(maxSeq, Number.parseInt(match[1]!, 10));
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
return "001";
|
|
438
|
+
}
|
|
439
|
+
return String(maxSeq + 1).padStart(3, "0");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function writeManualGuidedTerminalErrorActivity(
|
|
443
|
+
basePath: string,
|
|
444
|
+
unitType: string,
|
|
445
|
+
model: string,
|
|
446
|
+
detail: string,
|
|
447
|
+
stopReason: unknown,
|
|
448
|
+
): void {
|
|
449
|
+
const activityDir = join(gsdRoot(basePath), "activity");
|
|
450
|
+
mkdirSync(activityDir, { recursive: true });
|
|
451
|
+
const seq = nextManualActivitySequence(activityDir);
|
|
452
|
+
const safeUnitType = unitType.replace(/[^a-z0-9_.-]+/gi, "-");
|
|
453
|
+
const markerPath = join(activityDir, `${seq}-${safeUnitType}-manual-terminal-provider-error.jsonl`);
|
|
454
|
+
const message = `Manual guided ${unitType} turn ended with provider ${String(stopReason)} on ${model}: ${detail}`;
|
|
455
|
+
writeFileSync(
|
|
456
|
+
markerPath,
|
|
457
|
+
JSON.stringify({
|
|
458
|
+
type: "message",
|
|
459
|
+
message: {
|
|
460
|
+
role: "toolResult",
|
|
461
|
+
toolCallId: "manual-guided-terminal-provider-error",
|
|
462
|
+
toolName: "provider",
|
|
463
|
+
isError: true,
|
|
464
|
+
content: [{ type: "text", text: message }],
|
|
465
|
+
},
|
|
466
|
+
}) + "\n",
|
|
467
|
+
"utf-8",
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function observeManualDiscussTerminalError(
|
|
472
|
+
ctx: ExtensionContext,
|
|
473
|
+
lastMsg: unknown,
|
|
474
|
+
guidedUnit: GuidedUnitContext | null,
|
|
475
|
+
): void {
|
|
476
|
+
if (!guidedUnit?.unitType.startsWith("discuss-")) return;
|
|
477
|
+
if (!isFatalManualGuidedTerminalFailure(lastMsg)) return;
|
|
478
|
+
|
|
479
|
+
const model = modelLabel(ctx);
|
|
480
|
+
const detail = terminalFailureDetail(lastMsg);
|
|
481
|
+
ctx.ui.notify(
|
|
482
|
+
`Manual /gsd discuss ${guidedUnit.unitType} ended with a provider error on ${model}: ${detail}`,
|
|
483
|
+
"warning",
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
writeManualGuidedTerminalErrorActivity(
|
|
488
|
+
guidedUnit.basePath,
|
|
489
|
+
guidedUnit.unitType,
|
|
490
|
+
model,
|
|
491
|
+
detail,
|
|
492
|
+
isObjectRecord(lastMsg) ? lastMsg.stopReason : "unknown",
|
|
493
|
+
);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
496
|
+
logWarning("bootstrap", `Failed to write manual guided terminal-error activity marker: ${message}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
391
500
|
async function pauseTransientWithBackoff(
|
|
392
501
|
cls: ErrorClass,
|
|
393
502
|
pi: ExtensionAPI,
|
|
@@ -437,7 +546,9 @@ export async function handleAgentEnd(
|
|
|
437
546
|
// rejected" loop even though the files are on disk.
|
|
438
547
|
clearPathCache();
|
|
439
548
|
const basePath = resolveAgentEndBasePath();
|
|
440
|
-
|
|
549
|
+
const lastMsg = event.messages[event.messages.length - 1];
|
|
550
|
+
const guidedUnit = basePath ? getGuidedUnitContext(basePath) ?? getGuidedUnitContext() : getGuidedUnitContext();
|
|
551
|
+
clearGuidedUnitContext(guidedUnit?.basePath ?? basePath);
|
|
441
552
|
|
|
442
553
|
try {
|
|
443
554
|
if (await checkDeepProjectSetupAfterTurn(event, ctx, basePath)) {
|
|
@@ -466,13 +577,15 @@ export async function handleAgentEnd(
|
|
|
466
577
|
// discussions (where isAutoActive may be false) still get recovered.
|
|
467
578
|
if (maybeHandleEmptyIntentTurn(event, isAutoActive(), basePath)) return;
|
|
468
579
|
|
|
469
|
-
if (!isAutoActive())
|
|
580
|
+
if (!isAutoActive()) {
|
|
581
|
+
observeManualDiscussTerminalError(ctx, lastMsg, guidedUnit);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
470
584
|
|
|
471
585
|
if (shouldIgnoreAgentEndForActiveUnit(event)) {
|
|
472
586
|
return;
|
|
473
587
|
}
|
|
474
588
|
|
|
475
|
-
const lastMsg = event.messages[event.messages.length - 1];
|
|
476
589
|
if (isSessionSwitchInFlight()) {
|
|
477
590
|
_handleSessionSwitchAgentEnd(lastMsg, resolveAgentEndCancelled);
|
|
478
591
|
return;
|
|
@@ -555,9 +668,9 @@ export async function handleAgentEnd(
|
|
|
555
668
|
});
|
|
556
669
|
return;
|
|
557
670
|
}
|
|
558
|
-
// #3588: When errorMessage is uninformative, extract the real error
|
|
559
|
-
//
|
|
560
|
-
//
|
|
671
|
+
// #3588/#956: When errorMessage is uninformative, extract the real error
|
|
672
|
+
// from assistant text. Prefer rawErrorMsg for classification to avoid
|
|
673
|
+
// prose false-positives, but use display text when rawErrorMsg is empty.
|
|
561
674
|
const displayMsg = resolveAgentEndErrorDisplay(
|
|
562
675
|
rawErrorMsg,
|
|
563
676
|
"content" in lastMsg ? lastMsg.content : undefined,
|
|
@@ -576,8 +689,8 @@ export async function handleAgentEnd(
|
|
|
576
689
|
const errorDetail = displayMsg ? `: ${displayMsg}` : "";
|
|
577
690
|
const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined;
|
|
578
691
|
|
|
579
|
-
// ── 1. Classify
|
|
580
|
-
const cls = classifyError(rawErrorMsg, explicitRetryAfterMs);
|
|
692
|
+
// ── 1. Classify, preserving non-empty errorMessage precedence ──────
|
|
693
|
+
const cls = classifyError(rawErrorMsg || displayMsg, explicitRetryAfterMs);
|
|
581
694
|
|
|
582
695
|
// ── 1a. Unsupported-model: provider rejected this model for the current
|
|
583
696
|
// account/plan at request time (#4513). Persist a block so the
|
|
@@ -655,7 +768,7 @@ export async function handleAgentEnd(
|
|
|
655
768
|
// Core retries transient failures in-session after this handler.
|
|
656
769
|
// Keep that behavior for non-rate-limit classes to avoid pause/retry races,
|
|
657
770
|
// but let rate-limit continue into model fallback logic below (#4373).
|
|
658
|
-
if (shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg)) {
|
|
771
|
+
if (shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg, rawErrorMsg || displayMsg)) {
|
|
659
772
|
return;
|
|
660
773
|
}
|
|
661
774
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Type, StringEnum } from "@gsd/pi-ai";
|
|
4
4
|
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
5
5
|
import { Text } from "@gsd/pi-tui";
|
|
6
|
+
import { SUMMARY_SAVE_CONTENT_MAX_LENGTH } from "@opengsd/contracts";
|
|
6
7
|
|
|
7
8
|
import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
8
9
|
import { ensureDbOpen, resolveCtxCwd, resolveWorkflowToolBasePath } from "./dynamic-tools.js";
|
|
@@ -392,13 +393,17 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
392
393
|
"Root-level artifact paths are PROJECT.md, PROJECT-DRAFT.md, REQUIREMENTS.md, and REQUIREMENTS-DRAFT.md.",
|
|
393
394
|
"artifact_type must be one of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT, PROJECT, PROJECT-DRAFT, REQUIREMENTS, REQUIREMENTS-DRAFT.",
|
|
394
395
|
"Use CONTEXT-DRAFT for incremental draft persistence; use CONTEXT for the final milestone context after depth verification.",
|
|
396
|
+
`Keep each content payload under ${SUMMARY_SAVE_CONTENT_MAX_LENGTH} characters; save large context incrementally with CONTEXT-DRAFT/PROJECT-DRAFT/REQUIREMENTS-DRAFT instead of one oversized call.`,
|
|
395
397
|
],
|
|
396
398
|
parameters: Type.Object({
|
|
397
399
|
milestone_id: Type.Optional(Type.String({ description: "Milestone ID (e.g. M001). Omit only for root-level PROJECT/PROJECT-DRAFT/REQUIREMENTS/REQUIREMENTS-DRAFT artifacts." })),
|
|
398
400
|
slice_id: Type.Optional(Type.String({ description: "Slice ID (e.g. S01)" })),
|
|
399
401
|
task_id: Type.Optional(Type.String({ description: "Task ID (e.g. T01)" })),
|
|
400
402
|
artifact_type: StringEnum(["SUMMARY", "RESEARCH", "CONTEXT", "ASSESSMENT", "CONTEXT-DRAFT", "PROJECT", "PROJECT-DRAFT", "REQUIREMENTS", "REQUIREMENTS-DRAFT"], { description: "Artifact type to save" }),
|
|
401
|
-
content: Type.String({
|
|
403
|
+
content: Type.String({
|
|
404
|
+
description: `The full markdown content of the artifact. Maximum ${SUMMARY_SAVE_CONTENT_MAX_LENGTH} characters per save.`,
|
|
405
|
+
maxLength: SUMMARY_SAVE_CONTENT_MAX_LENGTH,
|
|
406
|
+
}),
|
|
402
407
|
}),
|
|
403
408
|
execute: summarySaveExecute,
|
|
404
409
|
renderCall(args: any, theme: any) {
|
|
@@ -72,6 +72,7 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
72
72
|
return executeUatExec(params as Parameters<typeof executeUatExec>[0], {
|
|
73
73
|
baseDir,
|
|
74
74
|
preferences: await loadContextModePreferences(baseDir),
|
|
75
|
+
signal: _signal,
|
|
75
76
|
});
|
|
76
77
|
},
|
|
77
78
|
});
|
|
@@ -119,6 +120,7 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
119
120
|
return executeGsdExec(params as Parameters<typeof executeGsdExec>[0], {
|
|
120
121
|
baseDir,
|
|
121
122
|
preferences: await loadContextModePreferences(baseDir),
|
|
123
|
+
signal: _signal,
|
|
122
124
|
});
|
|
123
125
|
},
|
|
124
126
|
});
|
|
@@ -13,7 +13,7 @@ import type { GSDEcosystemBeforeAgentStartHandler } from "../ecosystem/gsd-exten
|
|
|
13
13
|
import { updateSnapshot } from "../ecosystem/gsd-extension-api.js";
|
|
14
14
|
|
|
15
15
|
import { buildMilestoneFileName, canonicalPhaseDirName, clearPathCache, milestonesDir, legacyMilestonesDir, resolveMilestonePath, resolveSliceFile, resolveSlicePath } from "../paths.js";
|
|
16
|
-
import { applyAskUserQuestionsGateResult, clearDiscussionFlowState, formatPendingAskUserQuestionsGateMessage, formatTimedOutAskUserQuestionsGateMessage, hostWriteGateAdapter, isApprovalGateVerifiedInSnapshot, isDepthConfirmationAnswer,
|
|
16
|
+
import { applyAskUserQuestionsGateResult, clearDiscussionFlowState, currentWriteGateSnapshot, formatPendingAskUserQuestionsGateMessage, formatTimedOutAskUserQuestionsGateMessage, hostWriteGateAdapter, isApprovalGateVerifiedInSnapshot, isDepthConfirmationAnswer, isMilestoneDepthVerifiedInSnapshot, isQueuePhaseActive, resetWriteGateState, shouldBlockContextWrite, shouldBlockPlanningUnit, shouldBlockQueueExecution, shouldBlockWorktreeBash, shouldBlockWorktreeWrite, isGateQuestionId, getPendingGate, shouldBlockPendingGate, shouldBlockPendingGateBash, extractDepthVerificationMilestoneId, type WriteGateSnapshot } from "./write-gate.js";
|
|
17
17
|
import { canonicalToolName } from "../engine-hook-contract.js";
|
|
18
18
|
import { resolveManifest } from "../unit-context-manifest.js";
|
|
19
19
|
import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
|
|
@@ -583,6 +583,10 @@ function deferApprovalGate(gateId: string, basePath: string): void {
|
|
|
583
583
|
// workflow MCP child already verified this gate, deferring would block
|
|
584
584
|
// tools for a gate that can never legitimately arm.
|
|
585
585
|
const snapshot = hostWriteGateAdapter.readState(basePath);
|
|
586
|
+
deferApprovalGateFromSnapshot(gateId, basePath, snapshot);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function deferApprovalGateFromSnapshot(gateId: string, basePath: string, snapshot: WriteGateSnapshot): void {
|
|
586
590
|
if (isApprovalGateVerifiedInSnapshot(snapshot, gateId)) return;
|
|
587
591
|
const milestoneId = extractDepthVerificationMilestoneId(gateId);
|
|
588
592
|
if (milestoneId && isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId)) return;
|
|
@@ -1233,13 +1237,15 @@ export function registerHooks(
|
|
|
1233
1237
|
|
|
1234
1238
|
const gateId = approvalGateIdForUnit(unitType, unitId);
|
|
1235
1239
|
if (gateId) {
|
|
1240
|
+
const basePath = contextBasePath(ctx);
|
|
1241
|
+
const gateSnapshot = currentWriteGateSnapshot(basePath);
|
|
1236
1242
|
// Skip the gate if this milestone is already depth-verified — the approval
|
|
1237
1243
|
// pattern matched again on post-verification text (a false-positive re-trigger).
|
|
1238
1244
|
// Without this guard, the second firing blocks gsd_plan_milestone in the same
|
|
1239
1245
|
// turn and leaves CONTEXT.md on disk with no DB row (#discuss-milestone-no-db).
|
|
1240
1246
|
const gateMilestoneId = extractDepthVerificationMilestoneId(gateId);
|
|
1241
|
-
if (gateMilestoneId &&
|
|
1242
|
-
|
|
1247
|
+
if (gateMilestoneId && isMilestoneDepthVerifiedInSnapshot(gateSnapshot, gateMilestoneId)) return;
|
|
1248
|
+
deferApprovalGateFromSnapshot(gateId, basePath, gateSnapshot);
|
|
1243
1249
|
}
|
|
1244
1250
|
|
|
1245
1251
|
approvalQuestionAbortInFlight = true;
|
|
@@ -28,9 +28,10 @@ const DEFAULT_CODEBASE_MAX_CHARS = 8_000;
|
|
|
28
28
|
const MIN_CONTEXT_MESSAGE_MAX_CHARS = 1_000;
|
|
29
29
|
const MIN_KNOWLEDGE_MAX_CHARS = 1_000;
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
31
|
+
const CONTEXT_MAINTENANCE_KEY_SEPARATOR = "\0";
|
|
32
|
+
const contextMaintenanceCompletedForSession = new Set<string>();
|
|
33
|
+
const contextMaintenanceInFlightBySession = new Map<string, Promise<boolean>>();
|
|
34
|
+
const deferredContextMaintenanceBySession = new Map<string, Promise<void>>();
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Bundled skill triggers — resolved dynamically at runtime instead of
|
|
@@ -114,7 +115,8 @@ async function runSessionStartupMaintenanceOnce(
|
|
|
114
115
|
basePath: string,
|
|
115
116
|
ctx: ExtensionContext,
|
|
116
117
|
): Promise<boolean> {
|
|
117
|
-
|
|
118
|
+
const maintenanceKey = getContextMaintenanceKey(basePath, ctx);
|
|
119
|
+
if (contextMaintenanceCompletedForSession.has(maintenanceKey)) {
|
|
118
120
|
// Backfills are session-once, but memory queries and other DB-backed
|
|
119
121
|
// prompt assembly still need an active adapter on every turn.
|
|
120
122
|
try {
|
|
@@ -126,16 +128,16 @@ async function runSessionStartupMaintenanceOnce(
|
|
|
126
128
|
return false;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
const existing =
|
|
131
|
+
const existing = contextMaintenanceInFlightBySession.get(maintenanceKey);
|
|
130
132
|
const isInitiator = !existing;
|
|
131
133
|
// Use a definite Promise<boolean> so `await inFlight` has a known return type.
|
|
132
134
|
let inFlight: Promise<boolean>;
|
|
133
135
|
if (isInitiator) {
|
|
134
|
-
inFlight = performSessionStartupMaintenance(basePath, ctx);
|
|
135
|
-
|
|
136
|
+
inFlight = performSessionStartupMaintenance(basePath, ctx, maintenanceKey);
|
|
137
|
+
contextMaintenanceInFlightBySession.set(maintenanceKey, inFlight);
|
|
136
138
|
void inFlight.finally(() => {
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
+
if (contextMaintenanceInFlightBySession.get(maintenanceKey) === inFlight) {
|
|
140
|
+
contextMaintenanceInFlightBySession.delete(maintenanceKey);
|
|
139
141
|
}
|
|
140
142
|
});
|
|
141
143
|
} else {
|
|
@@ -149,6 +151,7 @@ async function runSessionStartupMaintenanceOnce(
|
|
|
149
151
|
async function performSessionStartupMaintenance(
|
|
150
152
|
basePath: string,
|
|
151
153
|
ctx: ExtensionContext,
|
|
154
|
+
maintenanceKey: string,
|
|
152
155
|
): Promise<boolean> {
|
|
153
156
|
// DB-backed memory backfills run below. On a cold session the database file
|
|
154
157
|
// may exist without an active in-process adapter, so open the canonical
|
|
@@ -172,11 +175,40 @@ async function performSessionStartupMaintenance(
|
|
|
172
175
|
|
|
173
176
|
// Mark session complete before scheduling deferred work so any concurrent
|
|
174
177
|
// caller that observes the completed state does not re-enter maintenance.
|
|
175
|
-
|
|
176
|
-
scheduleDeferredContextMaintenance(basePath);
|
|
178
|
+
contextMaintenanceCompletedForSession.add(maintenanceKey);
|
|
179
|
+
scheduleDeferredContextMaintenance(basePath, maintenanceKey);
|
|
177
180
|
return true;
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
function getContextMaintenanceKey(basePath: string, ctx: ExtensionContext): string {
|
|
184
|
+
return `${basePath}${CONTEXT_MAINTENANCE_KEY_SEPARATOR}${getContextSessionPart(ctx)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getContextSessionPart(ctx: ExtensionContext): string {
|
|
188
|
+
const sessionManager = (ctx as {
|
|
189
|
+
sessionManager?: {
|
|
190
|
+
getSessionId?: () => string | undefined;
|
|
191
|
+
getSessionFile?: () => string | undefined;
|
|
192
|
+
};
|
|
193
|
+
}).sessionManager;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const sessionId = sessionManager?.getSessionId?.();
|
|
197
|
+
if (typeof sessionId === "string" && sessionId.length > 0) return `id:${sessionId}`;
|
|
198
|
+
} catch (e) {
|
|
199
|
+
logWarning("bootstrap", `session-id fetch failed: ${(e as Error).message}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const sessionFile = sessionManager?.getSessionFile?.();
|
|
204
|
+
if (typeof sessionFile === "string" && sessionFile.length > 0) return `file:${sessionFile}`;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
logWarning("bootstrap", `session-file fetch failed: ${(e as Error).message}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return "process";
|
|
210
|
+
}
|
|
211
|
+
|
|
180
212
|
async function runDecisionsMemoryBackfill(ctx: ExtensionContext): Promise<void> {
|
|
181
213
|
// ADR-013 step 5: opportunistic decisions->memories backfill. Idempotent
|
|
182
214
|
// and best-effort — first run absorbs the existing decisions table into
|
|
@@ -210,8 +242,8 @@ async function runKnowledgeMemoryBackfill(
|
|
|
210
242
|
}
|
|
211
243
|
}
|
|
212
244
|
|
|
213
|
-
function scheduleDeferredContextMaintenance(basePath: string): void {
|
|
214
|
-
if (
|
|
245
|
+
function scheduleDeferredContextMaintenance(basePath: string, maintenanceKey: string): void {
|
|
246
|
+
if (deferredContextMaintenanceBySession.has(maintenanceKey)) return;
|
|
215
247
|
|
|
216
248
|
const task = new Promise<void>((resolve) => {
|
|
217
249
|
setTimeout(() => {
|
|
@@ -219,10 +251,10 @@ function scheduleDeferredContextMaintenance(basePath: string): void {
|
|
|
219
251
|
}, 0);
|
|
220
252
|
});
|
|
221
253
|
|
|
222
|
-
|
|
254
|
+
deferredContextMaintenanceBySession.set(maintenanceKey, task);
|
|
223
255
|
void task.finally(() => {
|
|
224
|
-
if (
|
|
225
|
-
|
|
256
|
+
if (deferredContextMaintenanceBySession.get(maintenanceKey) === task) {
|
|
257
|
+
deferredContextMaintenanceBySession.delete(maintenanceKey);
|
|
226
258
|
}
|
|
227
259
|
});
|
|
228
260
|
}
|
|
@@ -257,8 +289,10 @@ async function reportConsolidationGapsDeferred(basePath: string): Promise<void>
|
|
|
257
289
|
|
|
258
290
|
export async function _flushDeferredContextMaintenanceForTest(basePath?: string): Promise<void> {
|
|
259
291
|
const tasks = basePath
|
|
260
|
-
? [
|
|
261
|
-
|
|
292
|
+
? [...deferredContextMaintenanceBySession.entries()]
|
|
293
|
+
.filter(([key]) => key.startsWith(`${basePath}${CONTEXT_MAINTENANCE_KEY_SEPARATOR}`))
|
|
294
|
+
.map(([, task]) => task)
|
|
295
|
+
: [...deferredContextMaintenanceBySession.values()];
|
|
262
296
|
await Promise.allSettled(tasks);
|
|
263
297
|
}
|
|
264
298
|
|