@gajae-code/coding-agent 0.12.11 → 0.12.12
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 +38 -0
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +24 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +23 -5
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/update-cli.ts +90 -23
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +18 -0
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +31 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/main.ts +203 -67
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/utils/host-status.ts +38 -0
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +16 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/index.ts +70 -0
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +24 -7
- package/src/sdk/bus/telegram-daemon.ts +1961 -497
- package/src/sdk/bus/telegram-reference.ts +42 -6
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +139 -28
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +220 -9
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +206 -7
- package/src/session/session-manager.ts +148 -38
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/index.ts +8 -0
- package/src/utils/npm-registry.ts +660 -0
package/src/cli/update-cli.ts
CHANGED
|
@@ -12,6 +12,12 @@ import { $ } from "bun";
|
|
|
12
12
|
import chalk from "chalk";
|
|
13
13
|
import { installDefaultGjcDefinitions } from "../defaults/gjc-defaults";
|
|
14
14
|
import { theme } from "../modes/theme/theme";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_NPM_REGISTRY,
|
|
17
|
+
fetchLatestPackageVersion,
|
|
18
|
+
type Installer,
|
|
19
|
+
type NpmRegistryLookupOptions,
|
|
20
|
+
} from "../utils/npm-registry";
|
|
15
21
|
|
|
16
22
|
const RELEASE_REPO = "Yeachan-Heo/gajae-code";
|
|
17
23
|
const PACKAGE = "@gajae-code/coding-agent";
|
|
@@ -21,6 +27,10 @@ const NPM_MANAGED_PACKAGES = [NPM_WRAPPER_PACKAGE, PACKAGE] as const;
|
|
|
21
27
|
interface ReleaseInfo {
|
|
22
28
|
tag: string;
|
|
23
29
|
version: string;
|
|
30
|
+
/** Registry the version came from. Release binaries still come from GitHub. */
|
|
31
|
+
registry: string;
|
|
32
|
+
/** Config problems that did not stop the lookup but changed its outcome. */
|
|
33
|
+
warnings: string[];
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
/** Result from running the installed binary and parsing its reported version. */
|
|
@@ -205,23 +215,31 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
205
215
|
/**
|
|
206
216
|
* Get the latest release info from the npm registry.
|
|
207
217
|
* Uses npm instead of GitHub API to avoid unauthenticated rate limiting.
|
|
218
|
+
*
|
|
219
|
+
* The registry comes from npm config (`npm_config_registry`, `.npmrc`,
|
|
220
|
+
* `BUN_CONFIG_REGISTRY`) so the check reaches the same place the install does.
|
|
221
|
+
* Hardcoding the public registry broke every mirrored or firewalled network.
|
|
208
222
|
*/
|
|
209
|
-
async function getLatestRelease(): Promise<ReleaseInfo> {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const version = data.version;
|
|
217
|
-
const tag = `v${version}`;
|
|
223
|
+
async function getLatestRelease(options?: Installer | NpmRegistryLookupOptions): Promise<ReleaseInfo> {
|
|
224
|
+
const overrides: NpmRegistryLookupOptions = typeof options === "string" ? { installer: options } : (options ?? {});
|
|
225
|
+
// The user is deliberately waiting on this command, unlike the startup check.
|
|
226
|
+
const { version, registry, warnings } = await fetchLatestPackageVersion(PACKAGE, {
|
|
227
|
+
timeoutMs: 20_000,
|
|
228
|
+
...overrides,
|
|
229
|
+
});
|
|
218
230
|
|
|
219
231
|
return {
|
|
220
|
-
tag
|
|
232
|
+
tag: `v${version}`,
|
|
221
233
|
version,
|
|
234
|
+
registry,
|
|
235
|
+
warnings,
|
|
222
236
|
};
|
|
223
237
|
}
|
|
224
238
|
|
|
239
|
+
export function getLatestReleaseForTest(options: NpmRegistryLookupOptions): Promise<ReleaseInfo> {
|
|
240
|
+
return getLatestRelease(options);
|
|
241
|
+
}
|
|
242
|
+
|
|
225
243
|
/**
|
|
226
244
|
* Compare semver versions. Returns:
|
|
227
245
|
* - negative if a < b
|
|
@@ -376,8 +394,10 @@ function formatBinaryDownloadFailureMessage(
|
|
|
376
394
|
url: string,
|
|
377
395
|
status: string | number,
|
|
378
396
|
platform: NodeJS.Platform = process.platform,
|
|
397
|
+
registryNote?: string,
|
|
379
398
|
): string {
|
|
380
|
-
|
|
399
|
+
const note = registryNote ? `\n${registryNote}` : "";
|
|
400
|
+
return `Download failed for ${binaryName} from ${url}: ${status}.${note}\n${formatManualUpdateInstructions(platform)}`;
|
|
381
401
|
}
|
|
382
402
|
|
|
383
403
|
export function formatBinaryDownloadFailureMessageForTest(
|
|
@@ -385,8 +405,9 @@ export function formatBinaryDownloadFailureMessageForTest(
|
|
|
385
405
|
url: string,
|
|
386
406
|
status: string | number,
|
|
387
407
|
platform: NodeJS.Platform = process.platform,
|
|
408
|
+
registryNote?: string,
|
|
388
409
|
): string {
|
|
389
|
-
return formatBinaryDownloadFailureMessage(binaryName, url, status, platform);
|
|
410
|
+
return formatBinaryDownloadFailureMessage(binaryName, url, status, platform, registryNote);
|
|
390
411
|
}
|
|
391
412
|
|
|
392
413
|
export function buildReleaseBinaryUrlForTest(
|
|
@@ -582,10 +603,23 @@ export async function fsyncFileForTest(filePath: string): Promise<void> {
|
|
|
582
603
|
* Download a release binary to a temp path, throwing a friendly error when the
|
|
583
604
|
* release asset cannot be fetched.
|
|
584
605
|
*/
|
|
585
|
-
async function downloadBinaryTo(
|
|
606
|
+
async function downloadBinaryTo(
|
|
607
|
+
url: string,
|
|
608
|
+
tempPath: string,
|
|
609
|
+
binaryName: string,
|
|
610
|
+
registryNote?: string,
|
|
611
|
+
): Promise<void> {
|
|
586
612
|
const response = await fetch(url, { redirect: "follow" });
|
|
587
613
|
if (!response.ok || !response.body) {
|
|
588
|
-
throw new Error(
|
|
614
|
+
throw new Error(
|
|
615
|
+
formatBinaryDownloadFailureMessage(
|
|
616
|
+
binaryName,
|
|
617
|
+
url,
|
|
618
|
+
response.statusText || response.status,
|
|
619
|
+
process.platform,
|
|
620
|
+
registryNote,
|
|
621
|
+
),
|
|
622
|
+
);
|
|
589
623
|
}
|
|
590
624
|
const fileStream = fs.createWriteStream(tempPath, { mode: 0o755 });
|
|
591
625
|
await pipeline(response.body, fileStream);
|
|
@@ -639,16 +673,29 @@ export async function runBinaryUpdateFlow(
|
|
|
639
673
|
});
|
|
640
674
|
}
|
|
641
675
|
|
|
676
|
+
/**
|
|
677
|
+
* Describe the registry a version came from, when it is not the public one.
|
|
678
|
+
*
|
|
679
|
+
* The binary update path downloads from GitHub release tags, so a version that
|
|
680
|
+
* only exists on a private mirror produces a bare 404 with nothing linking it
|
|
681
|
+
* back to the registry that named it.
|
|
682
|
+
*/
|
|
683
|
+
function formatRegistryProvenance(version: string, registry: string | undefined): string | undefined {
|
|
684
|
+
if (!registry || registry === DEFAULT_NPM_REGISTRY) return undefined;
|
|
685
|
+
return `Version ${version} was resolved from ${registry}, not ${DEFAULT_NPM_REGISTRY}; a version published only to that registry has no matching GitHub release asset.`;
|
|
686
|
+
}
|
|
687
|
+
|
|
642
688
|
/**
|
|
643
689
|
* Download a release binary to a target path, replacing an existing file.
|
|
644
690
|
*/
|
|
645
|
-
async function updateViaBinaryAt(targetPath: string, expectedVersion: string): Promise<void> {
|
|
691
|
+
async function updateViaBinaryAt(targetPath: string, expectedVersion: string, registry?: string): Promise<void> {
|
|
646
692
|
const binaryName = getBinaryName();
|
|
647
693
|
const url = buildReleaseBinaryUrl(expectedVersion);
|
|
694
|
+
const registryNote = formatRegistryProvenance(expectedVersion, registry);
|
|
648
695
|
console.log(chalk.dim(`Downloading ${binaryName}…`));
|
|
649
696
|
|
|
650
697
|
const verification = await runBinaryUpdateFlow(targetPath, url, expectedVersion, {
|
|
651
|
-
download: (downloadUrl, tempPath) => downloadBinaryTo(downloadUrl, tempPath, binaryName),
|
|
698
|
+
download: (downloadUrl, tempPath) => downloadBinaryTo(downloadUrl, tempPath, binaryName, registryNote),
|
|
652
699
|
fsync: fsyncFile,
|
|
653
700
|
replace: replaceBinaryForUpdate,
|
|
654
701
|
verifyInstalledVersion: verifyInstalledRuntime,
|
|
@@ -665,20 +712,20 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
|
|
|
665
712
|
* Run the update command.
|
|
666
713
|
*/
|
|
667
714
|
export interface UpdateCommandDependencies {
|
|
668
|
-
getLatestRelease?: () => Promise<ReleaseInfo>;
|
|
715
|
+
getLatestRelease?: (installer?: Installer) => Promise<ReleaseInfo>;
|
|
669
716
|
resolveUpdateTarget?: () => Promise<UpdateTarget>;
|
|
670
|
-
performUpdate?: (target: UpdateTarget, expectedVersion: string) => Promise<void>;
|
|
717
|
+
performUpdate?: (target: UpdateTarget, expectedVersion: string, registry?: string) => Promise<void>;
|
|
671
718
|
refreshInstalledDefaultSkills?: () => Promise<void>;
|
|
672
719
|
exit?: (code: number) => never;
|
|
673
720
|
}
|
|
674
721
|
|
|
675
|
-
async function performUpdate(target: UpdateTarget, expectedVersion: string): Promise<void> {
|
|
722
|
+
async function performUpdate(target: UpdateTarget, expectedVersion: string, registry?: string): Promise<void> {
|
|
676
723
|
if (target.method === "bun") {
|
|
677
724
|
await updateViaBun(expectedVersion);
|
|
678
725
|
} else if (target.method === "npm") {
|
|
679
726
|
await updateViaNpm(target.packageName, expectedVersion);
|
|
680
727
|
} else {
|
|
681
|
-
await updateViaBinaryAt(target.path, expectedVersion);
|
|
728
|
+
await updateViaBinaryAt(target.path, expectedVersion, registry);
|
|
682
729
|
}
|
|
683
730
|
}
|
|
684
731
|
|
|
@@ -694,14 +741,34 @@ export async function runUpdateCommand(
|
|
|
694
741
|
|
|
695
742
|
console.log(chalk.dim(`Current version: ${VERSION}`));
|
|
696
743
|
|
|
744
|
+
// Resolve the install target first so the registry lookup can match the
|
|
745
|
+
// manager that will actually run: the npm-managed path ignores
|
|
746
|
+
// BUN_CONFIG_REGISTRY, so preferring it there would make the version check
|
|
747
|
+
// disagree with the install this command is gating. Failure is not fatal
|
|
748
|
+
// here — the later resolveTarget() call reports it.
|
|
749
|
+
let installer: Installer | undefined;
|
|
750
|
+
let target: UpdateTarget | undefined;
|
|
751
|
+
try {
|
|
752
|
+
target = await resolveTarget();
|
|
753
|
+
installer = target.method === "bun" ? "bun" : target.method === "npm" ? "npm" : undefined;
|
|
754
|
+
} catch {
|
|
755
|
+
installer = undefined;
|
|
756
|
+
}
|
|
757
|
+
|
|
697
758
|
let release: ReleaseInfo;
|
|
698
759
|
try {
|
|
699
|
-
release = await lookupRelease();
|
|
760
|
+
release = await lookupRelease(installer);
|
|
700
761
|
} catch (err) {
|
|
701
762
|
console.error(chalk.red(`Failed to check for updates: ${err}`));
|
|
702
763
|
return exit(1);
|
|
703
764
|
}
|
|
704
765
|
|
|
766
|
+
// A config file that exists but could not be read changes which registry
|
|
767
|
+
// answered; saying so beats a version that quietly came from somewhere else.
|
|
768
|
+
// `?? []` because UpdateCommandDependencies is a public seam an untyped
|
|
769
|
+
// consumer can satisfy without the field.
|
|
770
|
+
for (const warning of release.warnings ?? []) console.warn(chalk.yellow(`Warning: ${warning}`));
|
|
771
|
+
|
|
705
772
|
const comparison = compareVersions(release.version, VERSION);
|
|
706
773
|
|
|
707
774
|
if (comparison <= 0 && !opts.force) {
|
|
@@ -718,8 +785,8 @@ export async function runUpdateCommand(
|
|
|
718
785
|
if (opts.check) return;
|
|
719
786
|
|
|
720
787
|
try {
|
|
721
|
-
const
|
|
722
|
-
await update(
|
|
788
|
+
const resolved = target ?? (await resolveTarget());
|
|
789
|
+
await update(resolved, release.version, release.registry);
|
|
723
790
|
} catch (err) {
|
|
724
791
|
console.error(chalk.red(`Update failed: ${err}`));
|
|
725
792
|
return exit(1);
|
package/src/commands/notify.ts
CHANGED
|
@@ -2,17 +2,32 @@
|
|
|
2
2
|
* Configure Telegram, Discord, or Slack notifications.
|
|
3
3
|
*/
|
|
4
4
|
import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
assertStrictActivateThreadInvocation,
|
|
7
|
+
assertStrictBindThreadInvocation,
|
|
8
|
+
type NotifyAction,
|
|
9
|
+
type NotifyCommandArgs,
|
|
10
|
+
runNotifyCliCommand,
|
|
11
|
+
} from "../cli/notify-cli";
|
|
6
12
|
import { initTheme } from "../modes/theme/theme";
|
|
7
13
|
|
|
8
|
-
const ACTIONS: NotifyAction[] = [
|
|
14
|
+
const ACTIONS: NotifyAction[] = [
|
|
15
|
+
"setup",
|
|
16
|
+
"status",
|
|
17
|
+
"health",
|
|
18
|
+
"test",
|
|
19
|
+
"recovery",
|
|
20
|
+
"bind-thread",
|
|
21
|
+
"activate-thread",
|
|
22
|
+
"daemon-internal",
|
|
23
|
+
];
|
|
9
24
|
|
|
10
25
|
export default class Notify extends Command {
|
|
11
26
|
static description = "Configure Telegram, Discord, or Slack notifications";
|
|
12
27
|
|
|
13
28
|
static args = {
|
|
14
29
|
action: Args.string({
|
|
15
|
-
description: "Notify action (setup|status|health|test|recovery|daemon-internal)",
|
|
30
|
+
description: "Notify action (setup|status|health|test|recovery|bind-thread|activate-thread|daemon-internal)",
|
|
16
31
|
required: false,
|
|
17
32
|
}),
|
|
18
33
|
extra: Args.string({
|
|
@@ -46,6 +61,8 @@ export default class Notify extends Command {
|
|
|
46
61
|
}),
|
|
47
62
|
probe: Flags.boolean({ description: "notify health: run the selected provider's REST diagnostic" }),
|
|
48
63
|
message: Flags.string({ description: "notify test: custom message body" }),
|
|
64
|
+
"session-id": Flags.string({ description: "Live GJC session to bind to an existing Slack thread" }),
|
|
65
|
+
"thread-ts": Flags.string({ description: "Existing Slack root thread timestamp" }),
|
|
49
66
|
"owner-id": Flags.string({ description: "Internal: daemon owner id" }),
|
|
50
67
|
"agent-dir": Flags.string({ description: "Internal: agent dir for the daemon" }),
|
|
51
68
|
};
|
|
@@ -105,8 +122,15 @@ export default class Notify extends Command {
|
|
|
105
122
|
redact: Boolean(flags.redact),
|
|
106
123
|
probe: Boolean(flags.probe),
|
|
107
124
|
message: flags.message as string | undefined,
|
|
125
|
+
sessionId: flagRec["session-id"] as string | undefined,
|
|
126
|
+
threadTs: flagRec["thread-ts"] as string | undefined,
|
|
108
127
|
};
|
|
109
128
|
|
|
129
|
+
// `bind-thread` and `activate-thread` have no positional or internal form:
|
|
130
|
+
// any extra argument or unrelated notify flag is rejected here rather than
|
|
131
|
+
// silently ignored.
|
|
132
|
+
if (action === "bind-thread") assertStrictBindThreadInvocation(cmd);
|
|
133
|
+
if (action === "activate-thread") assertStrictActivateThreadInvocation(cmd);
|
|
110
134
|
if (action !== "daemon-internal") await initTheme();
|
|
111
135
|
await runNotifyCliCommand(cmd);
|
|
112
136
|
}
|
package/src/commands/sdk.ts
CHANGED
|
@@ -423,6 +423,7 @@ export async function runSessionHost(
|
|
|
423
423
|
sessionManager: opened.sessionManager,
|
|
424
424
|
...(mcpConfigPath ? { mcpConfigPath } : {}),
|
|
425
425
|
...(mcpStartupTimeoutMs !== undefined ? { mcpStartupTimeoutMs } : {}),
|
|
426
|
+
...(request.readiness ? { readiness: request.readiness } : {}),
|
|
426
427
|
});
|
|
427
428
|
} catch (error) {
|
|
428
429
|
throw await registrationFailure(error);
|
|
@@ -61,6 +61,21 @@ function keptMalformedRecord(lockDir: string): GcRecord {
|
|
|
61
61
|
async function collectLockRecord(lockDir: string, ctx: GcContext): Promise<GcRecord> {
|
|
62
62
|
const info = await readFileLockInfoForGc(lockDir);
|
|
63
63
|
if (!info) return keptMalformedRecord(lockDir);
|
|
64
|
+
if (info.owner_host_id !== undefined) {
|
|
65
|
+
return {
|
|
66
|
+
store: "file_locks",
|
|
67
|
+
id: lockDir,
|
|
68
|
+
path: lockDir,
|
|
69
|
+
pid: info.pid,
|
|
70
|
+
pid_status: "unknown",
|
|
71
|
+
status: "host_qualified",
|
|
72
|
+
stale: false,
|
|
73
|
+
removable: false,
|
|
74
|
+
action: "none",
|
|
75
|
+
reason: "host_qualified_lock_requires_owner_reclamation",
|
|
76
|
+
detail: `timestamp=${info.timestamp}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
64
79
|
|
|
65
80
|
const probeResult = ctx.probe(info.pid);
|
|
66
81
|
const pidStatus = gcPidStatusLabel(probeResult);
|
|
@@ -166,6 +181,9 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
|
|
|
166
181
|
const lockDir = record.path ?? record.id;
|
|
167
182
|
const info = await readFileLockInfoForGc(lockDir);
|
|
168
183
|
if (!info) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
|
184
|
+
if (info.owner_host_id !== undefined) {
|
|
185
|
+
return { removed: false, skipped: "host_qualified_lock_requires_owner_reclamation" };
|
|
186
|
+
}
|
|
169
187
|
|
|
170
188
|
const probeResult = ctx.probe(info.pid);
|
|
171
189
|
if (probeResult.status !== "dead") {
|
package/src/config/file-lock.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
1
2
|
import type { Stats } from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
@@ -7,9 +8,11 @@ export interface FileLockOptions {
|
|
|
7
8
|
staleMs?: number;
|
|
8
9
|
retries?: number;
|
|
9
10
|
retryDelayMs?: number;
|
|
11
|
+
/** Stable host identity required to safely reclaim locks on a shared volume. */
|
|
12
|
+
ownerHostId?: string;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
const DEFAULT_OPTIONS: Required<FileLockOptions
|
|
15
|
+
const DEFAULT_OPTIONS: Required<Omit<FileLockOptions, "ownerHostId">> = {
|
|
13
16
|
staleMs: 10_000,
|
|
14
17
|
retries: 50,
|
|
15
18
|
retryDelayMs: 100,
|
|
@@ -61,8 +64,16 @@ function ownerIsAlive(owner: FileLockOwnerToken, startTimeCache?: Map<string, st
|
|
|
61
64
|
return currentStartTime === null || currentStartTime === owner.start_time;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
function
|
|
65
|
-
|
|
67
|
+
function lockInfo(ownerHostId?: string): LockInfo {
|
|
68
|
+
return {
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
start_time: currentProcessStartTime(),
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
...(ownerHostId === undefined ? {} : { owner_host_id: ownerHostId }),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeLockInfo(lockPath: string, info: LockInfo): Promise<LockInfo> {
|
|
66
77
|
return Bun.write(`${lockPath}/info`, JSON.stringify(info)).then(() => info);
|
|
67
78
|
}
|
|
68
79
|
|
|
@@ -76,17 +87,18 @@ async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
|
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
79
|
-
const { pid, start_time, timestamp } = parsed as Partial<LockInfo>;
|
|
90
|
+
const { pid, start_time, timestamp, owner_host_id } = parsed as Partial<LockInfo>;
|
|
80
91
|
if (
|
|
81
92
|
typeof pid !== "number" ||
|
|
82
93
|
!Number.isInteger(pid) ||
|
|
83
94
|
pid <= 0 ||
|
|
84
95
|
typeof timestamp !== "number" ||
|
|
85
96
|
!Number.isFinite(timestamp) ||
|
|
86
|
-
(start_time !== undefined && (typeof start_time !== "string" || !start_time))
|
|
97
|
+
(start_time !== undefined && (typeof start_time !== "string" || !start_time)) ||
|
|
98
|
+
(owner_host_id !== undefined && (typeof owner_host_id !== "string" || !owner_host_id))
|
|
87
99
|
)
|
|
88
100
|
return null;
|
|
89
|
-
return { pid, start_time, timestamp };
|
|
101
|
+
return { pid, start_time, timestamp, owner_host_id };
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
/** @internal */
|
|
@@ -98,7 +110,7 @@ export async function readFileLockInfoForGc(lockDir: string): Promise<FileLockOw
|
|
|
98
110
|
export interface FileLockOwnerToken {
|
|
99
111
|
pid: number;
|
|
100
112
|
start_time?: string;
|
|
101
|
-
|
|
113
|
+
owner_host_id?: string;
|
|
102
114
|
timestamp: number;
|
|
103
115
|
}
|
|
104
116
|
|
|
@@ -147,6 +159,7 @@ export async function removeFileLockDirForGc(
|
|
|
147
159
|
if (
|
|
148
160
|
current.pid !== expected.pid ||
|
|
149
161
|
(expected.start_time !== undefined && current.start_time !== expected.start_time) ||
|
|
162
|
+
current.owner_host_id !== expected.owner_host_id ||
|
|
150
163
|
current.timestamp !== expected.timestamp
|
|
151
164
|
) {
|
|
152
165
|
return "owner_changed";
|
|
@@ -187,6 +200,7 @@ function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
|
|
|
187
200
|
async function staleLockSnapshot(
|
|
188
201
|
lockPath: string,
|
|
189
202
|
staleMs: number,
|
|
203
|
+
ownerHostId?: string,
|
|
190
204
|
startTimeCache?: Map<string, string | null>,
|
|
191
205
|
): Promise<LockStaleSnapshot> {
|
|
192
206
|
let info: LockInfo | null;
|
|
@@ -199,6 +213,7 @@ async function staleLockSnapshot(
|
|
|
199
213
|
if (hasFsCode(error, "EPERM")) return { stale: false };
|
|
200
214
|
throw error;
|
|
201
215
|
}
|
|
216
|
+
if (!info && ownerHostId !== undefined) return { stale: false };
|
|
202
217
|
if (!info) {
|
|
203
218
|
try {
|
|
204
219
|
const stats = await fs.stat(lockPath);
|
|
@@ -210,6 +225,10 @@ async function staleLockSnapshot(
|
|
|
210
225
|
}
|
|
211
226
|
}
|
|
212
227
|
|
|
228
|
+
// A host-qualified lock may only be reclaimed after proving that its owner is
|
|
229
|
+
// local. Foreign and malformed host-qualified records fail closed: PID values
|
|
230
|
+
// and clocks are not meaningful across hosts.
|
|
231
|
+
if (ownerHostId !== undefined && info.owner_host_id !== ownerHostId) return { stale: false };
|
|
213
232
|
// Never reap a live owner by elapsed time: a long legitimate critical section must
|
|
214
233
|
// not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
|
|
215
234
|
// liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
|
|
@@ -239,18 +258,43 @@ async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSn
|
|
|
239
258
|
}
|
|
240
259
|
}
|
|
241
260
|
|
|
242
|
-
async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
|
|
261
|
+
async function tryAcquireLock(lockPath: string, ownerHostId?: string): Promise<LockInfo | null> {
|
|
243
262
|
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
244
263
|
const afterParentMkdir = FileLockTestHooks.afterParentMkdir;
|
|
245
264
|
if (afterParentMkdir) await afterParentMkdir(lockPath);
|
|
265
|
+
if (ownerHostId === undefined) {
|
|
266
|
+
try {
|
|
267
|
+
await fs.mkdir(lockPath);
|
|
268
|
+
return await writeLockInfo(lockPath, lockInfo());
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return null;
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const pendingPath = `${lockPath}.pending.${process.pid}.${crypto.randomUUID()}`;
|
|
276
|
+
const owner = lockInfo(ownerHostId);
|
|
246
277
|
try {
|
|
247
|
-
await fs.mkdir(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return
|
|
278
|
+
await fs.mkdir(pendingPath);
|
|
279
|
+
await writeLockInfo(pendingPath, owner);
|
|
280
|
+
try {
|
|
281
|
+
await fs.rename(pendingPath, lockPath);
|
|
282
|
+
return owner;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
285
|
+
if (code === "EEXIST" || code === "ENOTEMPTY") return null;
|
|
286
|
+
if (code === "EPERM") {
|
|
287
|
+
try {
|
|
288
|
+
await fs.stat(lockPath);
|
|
289
|
+
return null;
|
|
290
|
+
} catch (statError) {
|
|
291
|
+
if (!isEnoent(statError)) throw statError;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
252
295
|
}
|
|
253
|
-
|
|
296
|
+
} finally {
|
|
297
|
+
await fs.rm(pendingPath, { recursive: true, force: true }).catch(() => undefined);
|
|
254
298
|
}
|
|
255
299
|
}
|
|
256
300
|
|
|
@@ -259,14 +303,15 @@ async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise
|
|
|
259
303
|
if (outcome !== "removed") throw new Error(`Failed to release file lock: ${outcome}.`);
|
|
260
304
|
}
|
|
261
305
|
async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
|
|
306
|
+
if (options.ownerHostId !== undefined && !options.ownerHostId) throw new Error("ownerHostId must be non-empty");
|
|
262
307
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
263
308
|
const lockPath = getLockPath(filePath);
|
|
264
309
|
const contentionStartTimes = new Map<string, string | null>();
|
|
265
310
|
for (let attempt = 0; attempt < opts.retries; attempt++) {
|
|
266
|
-
const owner = await tryAcquireLock(lockPath);
|
|
311
|
+
const owner = await tryAcquireLock(lockPath, opts.ownerHostId);
|
|
267
312
|
if (owner) return () => releaseLock(lockPath, owner);
|
|
268
313
|
|
|
269
|
-
const stale = await staleLockSnapshot(lockPath, opts.staleMs, contentionStartTimes);
|
|
314
|
+
const stale = await staleLockSnapshot(lockPath, opts.staleMs, opts.ownerHostId, contentionStartTimes);
|
|
270
315
|
if (await removeStaleLockForAcquire(lockPath, stale)) continue;
|
|
271
316
|
await Bun.sleep(opts.retryDelayMs);
|
|
272
317
|
}
|
|
@@ -243,25 +243,25 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
243
243
|
architect: "grok-build/grok-build",
|
|
244
244
|
}),
|
|
245
245
|
profile("cursor-eco", ["cursor"], {
|
|
246
|
-
default: "cursor/composer-
|
|
247
|
-
executor: "cursor/composer-
|
|
248
|
-
planner: "cursor/composer-
|
|
249
|
-
critic: "cursor/composer-
|
|
250
|
-
architect: "cursor/composer-
|
|
246
|
+
default: "cursor/composer-2.5",
|
|
247
|
+
executor: "cursor/composer-2.5",
|
|
248
|
+
planner: "cursor/composer-2.5",
|
|
249
|
+
critic: "cursor/composer-2.5",
|
|
250
|
+
architect: "cursor/composer-2.5",
|
|
251
251
|
}),
|
|
252
252
|
profile("cursor-medium", ["cursor"], {
|
|
253
|
-
default: "cursor/composer-
|
|
254
|
-
executor: "cursor/composer-
|
|
255
|
-
planner: "cursor/composer-
|
|
256
|
-
critic: "cursor/composer-
|
|
257
|
-
architect: "cursor/composer-
|
|
253
|
+
default: "cursor/composer-2.5",
|
|
254
|
+
executor: "cursor/composer-2.5-fast",
|
|
255
|
+
planner: "cursor/composer-2.5",
|
|
256
|
+
critic: "cursor/composer-2.5-fast",
|
|
257
|
+
architect: "cursor/composer-2.5-fast",
|
|
258
258
|
}),
|
|
259
259
|
profile("cursor-pro", ["cursor"], {
|
|
260
|
-
default: "cursor/composer-
|
|
261
|
-
executor: "cursor/composer-
|
|
262
|
-
planner: "cursor/composer-
|
|
263
|
-
critic: "cursor/composer-
|
|
264
|
-
architect: "cursor/composer-
|
|
260
|
+
default: "cursor/composer-2.5-fast",
|
|
261
|
+
executor: "cursor/composer-2.5-fast",
|
|
262
|
+
planner: "cursor/composer-2.5-fast",
|
|
263
|
+
critic: "cursor/composer-2.5-fast",
|
|
264
|
+
architect: "cursor/composer-2.5-fast",
|
|
265
265
|
}),
|
|
266
266
|
profile("minimax-eco", ["minimax-code"], {
|
|
267
267
|
default: "minimax-code/minimax-m3:low",
|
|
@@ -305,6 +305,20 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
305
305
|
architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
|
|
306
306
|
critic: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
|
|
307
307
|
}),
|
|
308
|
+
profile("alibaba-token-plan-qwen-deepseek", ["alibaba-token-plan"], {
|
|
309
|
+
default: "alibaba-token-plan/qwen-3.8-max:high",
|
|
310
|
+
executor: "alibaba-token-plan/deepseek-v4-flash-0731:high",
|
|
311
|
+
planner: "alibaba-token-plan/deepseek-v4-flash-0731:max",
|
|
312
|
+
architect: "alibaba-token-plan/qwen-3.8-max:xhigh",
|
|
313
|
+
critic: "alibaba-token-plan/qwen-3.8-max:xhigh",
|
|
314
|
+
}),
|
|
315
|
+
profile("alibaba-token-plan-glm-deepseek", ["alibaba-token-plan"], {
|
|
316
|
+
default: "alibaba-token-plan/glm-5.2:high",
|
|
317
|
+
executor: "alibaba-token-plan/deepseek-v4-flash-0731:high",
|
|
318
|
+
planner: "alibaba-token-plan/deepseek-v4-flash-0731:max",
|
|
319
|
+
architect: "alibaba-token-plan/glm-5.2:xhigh",
|
|
320
|
+
critic: "alibaba-token-plan/glm-5.2:xhigh",
|
|
321
|
+
}),
|
|
308
322
|
profile("opus-codex", ["anthropic", "openai-codex"], {
|
|
309
323
|
default: "anthropic/claude-opus-5:xhigh",
|
|
310
324
|
executor: "openai-codex/gpt-5.6-terra:low",
|
|
@@ -370,6 +384,8 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
|
370
384
|
"alibaba-token-plan-balanced": { displayName: "Balanced", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
371
385
|
"alibaba-token-plan-pro": { displayName: "Pro", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
372
386
|
"alibaba-token-plan-qwenmaxxing": { displayName: "QwenMaxxing", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
387
|
+
"alibaba-token-plan-qwen-deepseek": { displayName: "Qwen + DeepSeek", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
388
|
+
"alibaba-token-plan-glm-deepseek": { displayName: "GLM + DeepSeek", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
373
389
|
"opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
|
|
374
390
|
"codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
|
|
375
391
|
"fable-opus-codex": { displayName: "Fable + Opus + Codex", providerGroup: "COMBOS" },
|
|
@@ -12,6 +12,7 @@ export const COORDINATOR_MCP_TOOL_NAMES = [
|
|
|
12
12
|
"gjc_coordinator_watch_events",
|
|
13
13
|
"gjc_coordinator_register_session",
|
|
14
14
|
"gjc_coordinator_start_session",
|
|
15
|
+
"gjc_coordinator_activate_session",
|
|
15
16
|
"gjc_coordinator_stop_session",
|
|
16
17
|
"gjc_coordinator_send_prompt",
|
|
17
18
|
"gjc_coordinator_submit_question_answer",
|
|
@@ -6,6 +6,8 @@ import type { PrivateAskGateCodecV1, PublicReason } from "./question-gate-codec"
|
|
|
6
6
|
|
|
7
7
|
export type CoordinatorSessionState =
|
|
8
8
|
| "booting"
|
|
9
|
+
/** Live and endpoint-addressable, but withholding readiness until activation. */
|
|
10
|
+
| "prepared"
|
|
9
11
|
| "ready_for_input"
|
|
10
12
|
| "running"
|
|
11
13
|
| "needs_user_input"
|