@gajae-code/coding-agent 0.15.6 → 0.16.0
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/CHANGELOG.md +59 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/session-runtime.d.ts +52 -0
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +186 -41
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -0
|
@@ -375,8 +375,10 @@ function buildSingleResource({
|
|
|
375
375
|
|
|
376
376
|
function formatFileLine(idx: number, file: PrDiffFile, repo: string, prNumber: number): string {
|
|
377
377
|
const stats = file.changeType === "binary" ? "(binary)" : `+${file.additions} -${file.deletions}`;
|
|
378
|
-
const
|
|
379
|
-
|
|
378
|
+
const escapedNote = file.pathEscaped ? " (escaped non-UTF-8 name)" : "";
|
|
379
|
+
const renameNote = file.oldPathEscaped ? " (escaped non-UTF-8 name)" : "";
|
|
380
|
+
const rename = file.oldPath ? ` (renamed from ${file.oldPath}${renameNote})` : "";
|
|
381
|
+
return `${idx}. ${file.path}${escapedNote} ${stats} [${file.changeType}]${rename}\n pr://${repo}/${prNumber}/diff/${idx}`;
|
|
380
382
|
}
|
|
381
383
|
|
|
382
384
|
async function fetchAndRenderPrDiff(
|
package/src/main.ts
CHANGED
|
@@ -39,7 +39,9 @@ import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory }
|
|
|
39
39
|
import { initializeWithSettings } from "./discovery";
|
|
40
40
|
import { exportFromFile } from "./export/html";
|
|
41
41
|
import type { ExtensionUIContext } from "./extensibility/extensions/types";
|
|
42
|
+
import { releaseLaunchWorktreeReservationAfterRegistration } from "./gjc-runtime/launch-worktree-reservation";
|
|
42
43
|
import { persistCoordinatorRuntimeInputReady } from "./gjc-runtime/session-state-sidecar";
|
|
44
|
+
import { assertMasterLaunchArgs, assertMasterLaunchDisposition, createMasterModeContext } from "./master-mode/context";
|
|
43
45
|
import type { AcpStartupOptions } from "./modes/acp/startup-options";
|
|
44
46
|
import type { SessionSelectionResult } from "./modes/components/session-selector";
|
|
45
47
|
import type { InteractiveMode } from "./modes/interactive-mode";
|
|
@@ -56,7 +58,7 @@ import {
|
|
|
56
58
|
discoverAuthStorage,
|
|
57
59
|
} from "./sdk";
|
|
58
60
|
import { processIncarnation } from "./sdk/broker/process-incarnation";
|
|
59
|
-
import { SessionIndex } from "./sdk/broker/session-index";
|
|
61
|
+
import { newMasterAttestationEpoch, resolveSessionLocator, SessionIndex } from "./sdk/broker/session-index";
|
|
60
62
|
import type { AgentSession } from "./session/agent-session";
|
|
61
63
|
import { SessionMigrationBusyError } from "./session/internal/session-open-errors";
|
|
62
64
|
import {
|
|
@@ -752,6 +754,9 @@ export async function runInteractiveMode(
|
|
|
752
754
|
if (
|
|
753
755
|
shouldOfferAutomaticOnboarding({
|
|
754
756
|
normalInteractive: true,
|
|
757
|
+
automation:
|
|
758
|
+
/^(?:1|true|yes|on)$/i.test(process.env.CI ?? "") ||
|
|
759
|
+
/^(?:1|true|yes|on)$/i.test(process.env.GJC_AUTOMATION ?? ""),
|
|
755
760
|
initialMessage,
|
|
756
761
|
initialMessages,
|
|
757
762
|
initialImages,
|
|
@@ -1366,6 +1371,7 @@ export async function runRootCommand(
|
|
|
1366
1371
|
deps: RunRootCommandDependencies = {},
|
|
1367
1372
|
): Promise<void> {
|
|
1368
1373
|
const parsedArgs = parsed;
|
|
1374
|
+
assertMasterLaunchArgs(parsedArgs);
|
|
1369
1375
|
let initialThemeInitialized = false;
|
|
1370
1376
|
let autoChdirApplied = false;
|
|
1371
1377
|
let bareResumeSessionManager: SessionManager | undefined;
|
|
@@ -1553,6 +1559,22 @@ export async function runRootCommand(
|
|
|
1553
1559
|
print: Boolean(parsedArgs.print),
|
|
1554
1560
|
mode: parsedArgs.mode,
|
|
1555
1561
|
});
|
|
1562
|
+
// Master admission runs unconditionally and BEFORE any session state is
|
|
1563
|
+
// opened or created. Nesting it under nonInteractiveError skipped it for a
|
|
1564
|
+
// non-TTY launch carrying prepared input, which resolves to autoPrint with no
|
|
1565
|
+
// error, so `--master <prompt>` proceeded as an ordinary auto-print run.
|
|
1566
|
+
try {
|
|
1567
|
+
assertMasterLaunchDisposition({
|
|
1568
|
+
master: parsedArgs.master,
|
|
1569
|
+
isInteractive: disposition.isInteractive,
|
|
1570
|
+
autoPrint: disposition.autoPrint,
|
|
1571
|
+
nonInteractiveError: disposition.nonInteractiveError,
|
|
1572
|
+
});
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
process.stderr.write(`${chalk.red(error instanceof Error ? error.message : "Invalid master launch")}\n`);
|
|
1575
|
+
if (!deps.suppressProcessExit) process.exitCode = 1;
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1556
1578
|
if (disposition.nonInteractiveError) {
|
|
1557
1579
|
process.stderr.write(`${chalk.red(disposition.nonInteractiveError)}\n`);
|
|
1558
1580
|
process.exit(1);
|
|
@@ -1676,6 +1698,14 @@ export async function runRootCommand(
|
|
|
1676
1698
|
}
|
|
1677
1699
|
}
|
|
1678
1700
|
}
|
|
1701
|
+
const masterModeContext =
|
|
1702
|
+
parsedArgs.master === true && sessionManager
|
|
1703
|
+
? createMasterModeContext(
|
|
1704
|
+
parsedArgs.masterScope ?? "repo",
|
|
1705
|
+
sessionManager.getSessionId(),
|
|
1706
|
+
newMasterAttestationEpoch(),
|
|
1707
|
+
)
|
|
1708
|
+
: undefined;
|
|
1679
1709
|
|
|
1680
1710
|
const { options: sessionOptions } = await logger.time(
|
|
1681
1711
|
"buildSessionOptions",
|
|
@@ -1742,6 +1772,7 @@ export async function runRootCommand(
|
|
|
1742
1772
|
sessionOptions.notificationHostModeSupported = isInteractive;
|
|
1743
1773
|
sessionOptions.sdkHostModeSupported = isInteractive;
|
|
1744
1774
|
sessionOptions.settings = settingsInstance;
|
|
1775
|
+
sessionOptions.masterModeContext = masterModeContext;
|
|
1745
1776
|
if (isInteractive && sessionOptions.mcpConfigPath) {
|
|
1746
1777
|
sessionOptions.deferMcpConfigStartup = true;
|
|
1747
1778
|
}
|
|
@@ -1826,7 +1857,7 @@ export async function runRootCommand(
|
|
|
1826
1857
|
const directSessionId = process.env.GJC_LIFECYCLE_REQUEST_ID ? undefined : sessionManager?.getSessionId();
|
|
1827
1858
|
if (directSessionId) {
|
|
1828
1859
|
const sessionIndex = new SessionIndex(settingsInstance.getAgentDir());
|
|
1829
|
-
const locator =
|
|
1860
|
+
const locator = await resolveSessionLocator(sessionManager?.getCwd() ?? cwd, settingsInstance.getAgentDir());
|
|
1830
1861
|
// A pid is reusable, so the broker's teardown fence needs this session's OS
|
|
1831
1862
|
// start incarnation recorded alongside the pid it publishes.
|
|
1832
1863
|
const directSessionIncarnation = processIncarnation(process.pid);
|
|
@@ -1837,7 +1868,31 @@ export async function runRootCommand(
|
|
|
1837
1868
|
endpointGeneration: 0,
|
|
1838
1869
|
pid: process.pid,
|
|
1839
1870
|
...(directSessionIncarnation ? { processIncarnation: directSessionIncarnation } : {}),
|
|
1871
|
+
...(masterModeContext && directSessionIncarnation
|
|
1872
|
+
? {
|
|
1873
|
+
masterRole: {
|
|
1874
|
+
version: 2,
|
|
1875
|
+
ownerSessionId: masterModeContext.ownerSessionId,
|
|
1876
|
+
launchPid: process.pid,
|
|
1877
|
+
launchProcessIncarnation: directSessionIncarnation,
|
|
1878
|
+
role: "master" as const,
|
|
1879
|
+
attestationEpoch: masterModeContext.attestationEpoch,
|
|
1880
|
+
},
|
|
1881
|
+
}
|
|
1882
|
+
: {}),
|
|
1840
1883
|
});
|
|
1884
|
+
if (locator.worktreeRoot !== null) {
|
|
1885
|
+
try {
|
|
1886
|
+
await releaseLaunchWorktreeReservationAfterRegistration(
|
|
1887
|
+
settingsInstance.getAgentDir(),
|
|
1888
|
+
locator.worktreeRoot,
|
|
1889
|
+
);
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
logger.warn("Failed to release launch worktree reservation after host registration", {
|
|
1892
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1841
1896
|
postmortem.register("direct-session-index", async () => {
|
|
1842
1897
|
await sessionIndex.append({
|
|
1843
1898
|
type: "host_unregistered",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import type { Args } from "../cli/args";
|
|
3
|
+
|
|
4
|
+
export type MasterScope = "repo" | "pwd" | "global";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Process-local master authority. Its capability accessor is deliberately the
|
|
8
|
+
* only way to reach the raw capability; this context is never serializable.
|
|
9
|
+
*/
|
|
10
|
+
export interface MasterModeContext {
|
|
11
|
+
readonly scope: MasterScope;
|
|
12
|
+
readonly ownerSessionId: string;
|
|
13
|
+
readonly attestationEpoch: string;
|
|
14
|
+
getCapability(): string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createMasterModeContext(
|
|
18
|
+
scope: MasterScope,
|
|
19
|
+
ownerSessionId: string,
|
|
20
|
+
attestationEpoch: string,
|
|
21
|
+
): MasterModeContext {
|
|
22
|
+
const capability = randomBytes(32).toString("base64url");
|
|
23
|
+
return { scope, ownerSessionId, attestationEpoch, getCapability: () => capability };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Reject routes that cannot establish an interactive, durable master session. */
|
|
27
|
+
export function assertMasterLaunchArgs(parsed: Args): void {
|
|
28
|
+
if (parsed.master !== true) return;
|
|
29
|
+
if (
|
|
30
|
+
parsed.print === true ||
|
|
31
|
+
parsed.mode !== undefined ||
|
|
32
|
+
parsed.export !== undefined ||
|
|
33
|
+
parsed.listModels !== undefined ||
|
|
34
|
+
parsed.noSession === true
|
|
35
|
+
) {
|
|
36
|
+
throw new Error("--master requires an interactive durable TUI launch");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function assertMasterLaunchDisposition(input: {
|
|
41
|
+
master: boolean | undefined;
|
|
42
|
+
isInteractive: boolean;
|
|
43
|
+
autoPrint: boolean;
|
|
44
|
+
nonInteractiveError?: string;
|
|
45
|
+
}): void {
|
|
46
|
+
if (input.master !== true) return;
|
|
47
|
+
if (!input.isInteractive || input.autoPrint || input.nonInteractiveError !== undefined) {
|
|
48
|
+
throw new Error("--master requires an interactive TTY launch");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { canonicalSessionCwd, sessionWorktreeRoot } from "../sdk/broker/session-index";
|
|
2
|
+
import type { MasterScope } from "./context";
|
|
3
|
+
import { collectMasterPeerSnapshot, type MasterPeerSnapshotLifecycle, renderMasterPeerSnapshot } from "./snapshot";
|
|
4
|
+
|
|
5
|
+
export const MASTER_PEER_SNAPSHOT_CUSTOM_TYPE = "master-peer-snapshot";
|
|
6
|
+
|
|
7
|
+
/** The injected message shape consumed by the agent-session contributor seam. */
|
|
8
|
+
export interface MasterPeerSnapshotMessage {
|
|
9
|
+
customType: string;
|
|
10
|
+
content: string;
|
|
11
|
+
display: false;
|
|
12
|
+
attribution: "agent";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MasterPeerSnapshotContributorInput {
|
|
16
|
+
readonly lifecycle: MasterPeerSnapshotLifecycle;
|
|
17
|
+
readonly ownerSessionId: string;
|
|
18
|
+
/** Current transcript/session identity; differs from ownerSessionId after a branch or switch. */
|
|
19
|
+
readonly getSessionId?: () => string;
|
|
20
|
+
readonly scope: MasterScope;
|
|
21
|
+
/** Live cwd accessor; resolved at injection time, not at registration. */
|
|
22
|
+
readonly getCwd: () => string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether a prior injection was persisted to the session branch. A persisted
|
|
25
|
+
* block proves a first request was accepted, so later turns never duplicate
|
|
26
|
+
* it; a pre-accept cancellation persists nothing and the next attempt
|
|
27
|
+
* re-collects a fresh snapshot.
|
|
28
|
+
*/
|
|
29
|
+
readonly hasPersistedInjection: () => boolean;
|
|
30
|
+
readonly onError?: (error: unknown) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Builds the one-shot before-agent-start contributor that injects the scoped,
|
|
35
|
+
* no-probe master peer snapshot immediately before the first accepted provider
|
|
36
|
+
* request. Idle masters never collect: the snapshot is gathered only when a
|
|
37
|
+
* prompt actually starts.
|
|
38
|
+
*/
|
|
39
|
+
export function createMasterPeerSnapshotContributor(
|
|
40
|
+
input: MasterPeerSnapshotContributorInput,
|
|
41
|
+
): () => Promise<MasterPeerSnapshotMessage | undefined> {
|
|
42
|
+
return async () => {
|
|
43
|
+
try {
|
|
44
|
+
if (input.hasPersistedInjection()) return undefined;
|
|
45
|
+
const anchorCwd = await canonicalSessionCwd(input.getCwd());
|
|
46
|
+
const snapshot = await collectMasterPeerSnapshot({
|
|
47
|
+
lifecycle: input.lifecycle,
|
|
48
|
+
actor: { id: input.ownerSessionId, namespace: "sdk:master-snapshot" },
|
|
49
|
+
ownerSessionId: input.ownerSessionId,
|
|
50
|
+
...(input.getSessionId === undefined ? {} : { currentSessionId: input.getSessionId() }),
|
|
51
|
+
scope: input.scope,
|
|
52
|
+
requestAnchor: { cwd: anchorCwd, worktreeRoot: await sessionWorktreeRoot(anchorCwd) },
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
customType: MASTER_PEER_SNAPSHOT_CUSTOM_TYPE,
|
|
56
|
+
content: renderMasterPeerSnapshot(snapshot),
|
|
57
|
+
display: false,
|
|
58
|
+
attribution: "agent",
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
input.onError?.(error);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ResolvedScopeV1,
|
|
3
|
+
resolveScopeRequest,
|
|
4
|
+
type ScopeNameV1,
|
|
5
|
+
type ScopeRequestV1,
|
|
6
|
+
type SdkSearchResultV1,
|
|
7
|
+
type SdkSearchRowV1,
|
|
8
|
+
sdkSearchResultV1,
|
|
9
|
+
} from "../sdk/broker/session-scope";
|
|
10
|
+
import type { SessionLifecycleActor, SessionListOutcome, SessionListRequest } from "../sdk/lifecycle/service";
|
|
11
|
+
|
|
12
|
+
export type MasterPeerSnapshot = {
|
|
13
|
+
readonly status: SdkSearchResultV1["status"];
|
|
14
|
+
readonly scope: ResolvedScopeV1;
|
|
15
|
+
readonly observedAt: string;
|
|
16
|
+
readonly indexSeq?: number;
|
|
17
|
+
readonly rows: readonly SdkSearchRowV1[];
|
|
18
|
+
readonly truncated?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const MASTER_PEER_SNAPSHOT_MAX_ROWS = 256;
|
|
22
|
+
const MASTER_PEER_SNAPSHOT_MAX_BYTES = 64 * 1024;
|
|
23
|
+
|
|
24
|
+
/** The scoped Broker surface needed for the one-time master peer snapshot. */
|
|
25
|
+
export interface MasterPeerSnapshotLifecycle {
|
|
26
|
+
list(request: Omit<SessionListRequest, "operation">): Promise<SessionListOutcome>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CollectMasterPeerSnapshotInput {
|
|
30
|
+
readonly lifecycle: MasterPeerSnapshotLifecycle;
|
|
31
|
+
readonly actor: SessionLifecycleActor;
|
|
32
|
+
readonly ownerSessionId: string;
|
|
33
|
+
readonly currentSessionId?: string;
|
|
34
|
+
readonly scope: ScopeNameV1;
|
|
35
|
+
readonly requestAnchor: ScopeRequestV1["requestAnchor"];
|
|
36
|
+
readonly timeoutMs?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function compareText(left: string, right: string): number {
|
|
40
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Orders by session ID, then the complete canonical locator as a deterministic
|
|
45
|
+
* tie-breaker. The Broker's `live` value is index evidence, not a fresh probe.
|
|
46
|
+
*/
|
|
47
|
+
function compareRows(left: SdkSearchRowV1, right: SdkSearchRowV1): number {
|
|
48
|
+
const id = compareText(left.id, right.id);
|
|
49
|
+
if (id !== 0) return id;
|
|
50
|
+
const cwd = compareText(left.locator.cwd, right.locator.cwd);
|
|
51
|
+
if (cwd !== 0) return cwd;
|
|
52
|
+
const worktreeRoot = compareText(left.locator.worktreeRoot ?? "", right.locator.worktreeRoot ?? "");
|
|
53
|
+
if (worktreeRoot !== 0) return worktreeRoot;
|
|
54
|
+
return compareText(left.locator.stateRoot, right.locator.stateRoot);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unavailableSnapshot(scope: ResolvedScopeV1): MasterPeerSnapshot {
|
|
58
|
+
return {
|
|
59
|
+
status: "unavailable",
|
|
60
|
+
scope,
|
|
61
|
+
observedAt: new Date().toISOString(),
|
|
62
|
+
rows: [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function matchesResolvedScope(left: ResolvedScopeV1, right: ResolvedScopeV1): boolean {
|
|
67
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Retains a row prefix only when its final escaped prompt block fits the budget. */
|
|
71
|
+
function boundSnapshotRows(
|
|
72
|
+
snapshot: Omit<MasterPeerSnapshot, "rows" | "truncated">,
|
|
73
|
+
rows: readonly SdkSearchRowV1[],
|
|
74
|
+
): {
|
|
75
|
+
rows: readonly SdkSearchRowV1[];
|
|
76
|
+
truncated: boolean;
|
|
77
|
+
} {
|
|
78
|
+
const retained: SdkSearchRowV1[] = [];
|
|
79
|
+
for (const [index, row] of rows.entries()) {
|
|
80
|
+
if (retained.length >= MASTER_PEER_SNAPSHOT_MAX_ROWS) return { rows: retained, truncated: true };
|
|
81
|
+
|
|
82
|
+
retained.push(row);
|
|
83
|
+
const willTruncate = index < rows.length - 1;
|
|
84
|
+
const candidate: MasterPeerSnapshot = {
|
|
85
|
+
...snapshot,
|
|
86
|
+
rows: retained,
|
|
87
|
+
...(willTruncate ? { truncated: true } : {}),
|
|
88
|
+
};
|
|
89
|
+
if (Buffer.byteLength(renderMasterPeerSnapshot(candidate), "utf8") > MASTER_PEER_SNAPSHOT_MAX_BYTES) {
|
|
90
|
+
retained.pop();
|
|
91
|
+
return { rows: retained, truncated: true };
|
|
92
|
+
}
|
|
93
|
+
if (willTruncate && retained.length === MASTER_PEER_SNAPSHOT_MAX_ROWS) return { rows: retained, truncated: true };
|
|
94
|
+
}
|
|
95
|
+
return { rows: retained, truncated: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Collects the first-request peer snapshot through scoped Broker session.list.
|
|
100
|
+
* It deliberately has no Router or endpoint-probe dependency.
|
|
101
|
+
*/
|
|
102
|
+
export async function collectMasterPeerSnapshot(input: CollectMasterPeerSnapshotInput): Promise<MasterPeerSnapshot> {
|
|
103
|
+
const request: ScopeRequestV1 = {
|
|
104
|
+
version: 1,
|
|
105
|
+
requested: input.scope,
|
|
106
|
+
requestAnchor: input.requestAnchor,
|
|
107
|
+
};
|
|
108
|
+
const resolvedScope = await resolveScopeRequest(request);
|
|
109
|
+
const outcome = await input.lifecycle.list({
|
|
110
|
+
actor: input.actor,
|
|
111
|
+
capability: "session.list",
|
|
112
|
+
target: { scope: request },
|
|
113
|
+
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
|
|
114
|
+
});
|
|
115
|
+
const result = sdkSearchResultV1(outcome.result);
|
|
116
|
+
if (!result || !matchesResolvedScope(result.scope, resolvedScope)) return unavailableSnapshot(resolvedScope);
|
|
117
|
+
const snapshot: Omit<MasterPeerSnapshot, "rows" | "truncated"> = {
|
|
118
|
+
status: result.status,
|
|
119
|
+
scope: result.scope,
|
|
120
|
+
observedAt: result.observedAt,
|
|
121
|
+
...(result.indexSeq === undefined ? {} : { indexSeq: result.indexSeq }),
|
|
122
|
+
};
|
|
123
|
+
const rows = result.rows
|
|
124
|
+
.filter(row => row.id !== (input.currentSessionId ?? input.ownerSessionId))
|
|
125
|
+
.sort(compareRows);
|
|
126
|
+
const bounded = boundSnapshotRows(snapshot, rows);
|
|
127
|
+
return {
|
|
128
|
+
...snapshot,
|
|
129
|
+
rows: bounded.rows,
|
|
130
|
+
...(bounded.truncated ? { truncated: true } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Escapes untrusted metadata before it is placed in fixed prompt framing. */
|
|
135
|
+
export function escapeMasterPeerSnapshotText(input: string): string {
|
|
136
|
+
let firstEscapable = -1;
|
|
137
|
+
for (let index = 0; index < input.length; index++) {
|
|
138
|
+
const code = input.charCodeAt(index);
|
|
139
|
+
if (code === 38 || code === 60 || code === 62 || code === 96) {
|
|
140
|
+
firstEscapable = index;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (firstEscapable === -1) return input;
|
|
145
|
+
|
|
146
|
+
let output = input.slice(0, firstEscapable);
|
|
147
|
+
for (let index = firstEscapable; index < input.length; index++) {
|
|
148
|
+
const character = input[index];
|
|
149
|
+
if (character === "&") output += "&";
|
|
150
|
+
else if (character === "<") output += "<";
|
|
151
|
+
else if (character === ">") output += ">";
|
|
152
|
+
else if (character === "`") output += "`";
|
|
153
|
+
else output += character;
|
|
154
|
+
}
|
|
155
|
+
return output;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function escapeOptionalText(input: string | null): string | null {
|
|
159
|
+
return input === null ? null : escapeMasterPeerSnapshotText(input);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderedScope(scope: ResolvedScopeV1): ResolvedScopeV1 {
|
|
163
|
+
const resolved =
|
|
164
|
+
scope.resolved === null
|
|
165
|
+
? null
|
|
166
|
+
: scope.resolved.kind === "repo"
|
|
167
|
+
? { kind: "repo" as const, worktreeRoot: escapeMasterPeerSnapshotText(scope.resolved.worktreeRoot) }
|
|
168
|
+
: scope.resolved.kind === "pwd"
|
|
169
|
+
? { kind: "pwd" as const, cwd: escapeMasterPeerSnapshotText(scope.resolved.cwd) }
|
|
170
|
+
: { kind: "global" as const, visibility: scope.resolved.visibility };
|
|
171
|
+
return {
|
|
172
|
+
version: scope.version,
|
|
173
|
+
requested: escapeMasterPeerSnapshotText(scope.requested) as ScopeNameV1,
|
|
174
|
+
requestAnchor: {
|
|
175
|
+
cwd: escapeMasterPeerSnapshotText(scope.requestAnchor.cwd),
|
|
176
|
+
worktreeRoot: escapeOptionalText(scope.requestAnchor.worktreeRoot),
|
|
177
|
+
},
|
|
178
|
+
resolved,
|
|
179
|
+
resolution: escapeMasterPeerSnapshotText(scope.resolution) as ResolvedScopeV1["resolution"],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderedRows(rows: readonly SdkSearchRowV1[]): readonly SdkSearchRowV1[] {
|
|
184
|
+
return rows.map(row => ({
|
|
185
|
+
id: escapeMasterPeerSnapshotText(row.id),
|
|
186
|
+
locator: {
|
|
187
|
+
cwd: escapeMasterPeerSnapshotText(row.locator.cwd),
|
|
188
|
+
worktreeRoot: escapeOptionalText(row.locator.worktreeRoot),
|
|
189
|
+
stateRoot: escapeMasterPeerSnapshotText(row.locator.stateRoot),
|
|
190
|
+
},
|
|
191
|
+
live: row.live,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Renders the fixed, escaped prompt block for one master peer observation. */
|
|
196
|
+
export function renderMasterPeerSnapshot(snapshot: MasterPeerSnapshot): string {
|
|
197
|
+
const content = {
|
|
198
|
+
scope: renderedScope(snapshot.scope),
|
|
199
|
+
observedAt: escapeMasterPeerSnapshotText(snapshot.observedAt),
|
|
200
|
+
...(snapshot.indexSeq === undefined ? {} : { indexSeq: snapshot.indexSeq }),
|
|
201
|
+
...(snapshot.truncated === undefined ? {} : { truncated: snapshot.truncated }),
|
|
202
|
+
rows: renderedRows(snapshot.rows),
|
|
203
|
+
};
|
|
204
|
+
return `<gjc-master-peer-snapshot>\nstatus: ${escapeMasterPeerSnapshotText(snapshot.status)}\n${JSON.stringify(content)}\n</gjc-master-peer-snapshot>`;
|
|
205
|
+
}
|