@gajae-code/coding-agent 0.12.10 → 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 +47 -2
- 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 +13 -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 +25 -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/internal/managed-session-storage.d.ts +12 -2
- package/dist/types/session/session-manager.d.ts +40 -5
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/todo-write.d.ts +2 -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 +13 -12
- 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/prompts/tools/computer.md +6 -1
- 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 +18 -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 +25 -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 +331 -30
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +440 -243
- package/src/session/session-manager.ts +288 -44
- package/src/session/session-storage.ts +3 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/computer.ts +322 -45
- package/src/tools/index.ts +8 -0
- package/src/tools/todo-write.ts +29 -11
- package/src/utils/npm-registry.ts +660 -0
package/src/cli/notify-cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
7
7
|
import { APP_NAME } from "@gajae-code/utils/dirs";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { Settings, type SettingsAtomicPatch } from "../config/settings";
|
|
10
|
+
import { SessionIndex } from "../sdk/broker/session-index";
|
|
10
11
|
import {
|
|
11
12
|
ChatDaemonController,
|
|
12
13
|
type EnsureChatDaemonResult,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
ensureSlackDaemon,
|
|
15
16
|
} from "../sdk/bus/chat-daemon-control";
|
|
16
17
|
import { getNotificationConfig, maskToken, tokenFingerprint } from "../sdk/bus/config";
|
|
18
|
+
import { type ActivatedPreparedSession, activatePreparedSession } from "../sdk/bus/existing-thread-readiness";
|
|
17
19
|
import {
|
|
18
20
|
clearTelegramActivationMarker,
|
|
19
21
|
createTelegramActivationMarker,
|
|
@@ -36,6 +38,12 @@ import {
|
|
|
36
38
|
sanitizeDiagnostic,
|
|
37
39
|
sendNotificationTest,
|
|
38
40
|
} from "../sdk/bus/notification-service";
|
|
41
|
+
import {
|
|
42
|
+
type BoundSlackThread,
|
|
43
|
+
bindConfiguredSlackThread,
|
|
44
|
+
isBoundedSlackRootTs,
|
|
45
|
+
SlackThreadBindingError,
|
|
46
|
+
} from "../sdk/bus/slack-thread-binding";
|
|
39
47
|
import {
|
|
40
48
|
type EnsureTelegramDaemonDetailedResult,
|
|
41
49
|
ensureTelegramDaemonRunningDetailed,
|
|
@@ -49,7 +57,15 @@ import {
|
|
|
49
57
|
type TelegramSetupTimers,
|
|
50
58
|
} from "../sdk/bus/telegram-setup";
|
|
51
59
|
|
|
52
|
-
export type NotifyAction =
|
|
60
|
+
export type NotifyAction =
|
|
61
|
+
| "setup"
|
|
62
|
+
| "status"
|
|
63
|
+
| "health"
|
|
64
|
+
| "test"
|
|
65
|
+
| "recovery"
|
|
66
|
+
| "bind-thread"
|
|
67
|
+
| "activate-thread"
|
|
68
|
+
| "daemon-internal";
|
|
53
69
|
export type NotifySetupProvider = "telegram" | "discord" | "slack";
|
|
54
70
|
|
|
55
71
|
export interface NotifyCommandArgs {
|
|
@@ -72,6 +88,8 @@ export interface NotifyCommandArgs {
|
|
|
72
88
|
forceDaemonLock?: boolean;
|
|
73
89
|
probe?: boolean;
|
|
74
90
|
message?: string;
|
|
91
|
+
sessionId?: string;
|
|
92
|
+
threadTs?: string;
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
export interface NotifyCommandDeps {
|
|
@@ -99,6 +117,8 @@ export interface NotifyCommandDeps {
|
|
|
99
117
|
setupPidIncarnation?: (pid: number) => string | undefined;
|
|
100
118
|
ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<EnsureChatDaemonResult>;
|
|
101
119
|
ensureTelegramDaemon?: (settings: Settings) => Promise<EnsureTelegramDaemonDetailedResult>;
|
|
120
|
+
bindSlackThread?: (input: { settings: Settings; sessionId: string; threadTs: string }) => Promise<BoundSlackThread>;
|
|
121
|
+
activatePreparedSession?: (input: { settings: Settings; sessionId: string }) => Promise<ActivatedPreparedSession>;
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
@@ -200,6 +220,23 @@ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
|
200
220
|
? { action, rawArgs: args.slice(2), forceDaemonLock: flags.get("--force-daemon-lock") === true }
|
|
201
221
|
: undefined;
|
|
202
222
|
}
|
|
223
|
+
if (action === "bind-thread") {
|
|
224
|
+
const rest = args.slice(2);
|
|
225
|
+
const flags = parseFlags(rest, new Set(["--session-id", "--thread-ts"]), new Set());
|
|
226
|
+
if (!flags) return undefined;
|
|
227
|
+
const sessionId = flags.get("--session-id");
|
|
228
|
+
const threadTs = flags.get("--thread-ts");
|
|
229
|
+
if (typeof sessionId !== "string" || typeof threadTs !== "string") return undefined;
|
|
230
|
+
return { action, rawArgs: rest, sessionId, threadTs };
|
|
231
|
+
}
|
|
232
|
+
if (action === "activate-thread") {
|
|
233
|
+
const rest = args.slice(2);
|
|
234
|
+
const flags = parseFlags(rest, new Set(["--session-id"]), new Set());
|
|
235
|
+
if (!flags) return undefined;
|
|
236
|
+
const sessionId = flags.get("--session-id");
|
|
237
|
+
if (typeof sessionId !== "string") return undefined;
|
|
238
|
+
return { action, rawArgs: rest, sessionId };
|
|
239
|
+
}
|
|
203
240
|
if (action === "daemon-internal") {
|
|
204
241
|
return {
|
|
205
242
|
action,
|
|
@@ -232,6 +269,12 @@ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyComma
|
|
|
232
269
|
case "recovery":
|
|
233
270
|
await runRecovery(deps, cmd.forceDaemonLock);
|
|
234
271
|
return;
|
|
272
|
+
case "bind-thread":
|
|
273
|
+
await runBindThread(cmd, deps);
|
|
274
|
+
return;
|
|
275
|
+
case "activate-thread":
|
|
276
|
+
await runActivateThread(cmd, deps);
|
|
277
|
+
return;
|
|
235
278
|
case "daemon-internal":
|
|
236
279
|
if (cmd.smoke) {
|
|
237
280
|
await runDaemonInternal(["--smoke"]);
|
|
@@ -457,6 +500,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
457
500
|
process.stdout.write(`Using provided chat id ${result.chatId} (non-interactive).\n`);
|
|
458
501
|
}
|
|
459
502
|
let settingsCommitted = false;
|
|
503
|
+
let commitAttempted = false;
|
|
460
504
|
try {
|
|
461
505
|
const proposedIdentity = deps.setupPreflight
|
|
462
506
|
? proposedIdentityFromSetupPreflight(deps.setupPreflight, token.trim(), result.chatId)
|
|
@@ -480,6 +524,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
480
524
|
{ path: "notifications.telegram.enabled", op: "set", value: true },
|
|
481
525
|
];
|
|
482
526
|
if (deps.setupRedact ?? cmd.redact) patches.push({ path: "notifications.redact", op: "set", value: true });
|
|
527
|
+
commitAttempted = true;
|
|
483
528
|
const receipt = await settings.commitAtomicBatch(patches);
|
|
484
529
|
settingsCommitted = true;
|
|
485
530
|
const activationMarker = createTelegramActivationMarker({
|
|
@@ -534,8 +579,24 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
534
579
|
receipt.discard();
|
|
535
580
|
} catch (error) {
|
|
536
581
|
const detail = sanitizeDiagnostic(error instanceof Error ? error.message : "unknown persistence failure", token);
|
|
582
|
+
// The wording must describe what a follow-up `notify status` will show. A failure
|
|
583
|
+
// raised after the durable write landed — including one raised from inside the
|
|
584
|
+
// commit itself — must not claim the settings were not persisted, or the operator
|
|
585
|
+
// walks away believing Telegram is off while the daemon is armed for that token.
|
|
586
|
+
// Observed state wins; the code-path flag is only the fallback for an unreadable read.
|
|
587
|
+
const observed = telegramIntentIsPersisted(settings, token.trim(), result.chatId);
|
|
588
|
+
const persisted = observed ?? settingsCommitted;
|
|
589
|
+
// A commit that was entered and then failed, whose durable state is also unreadable,
|
|
590
|
+
// is genuinely undecided: `commitAtomicBatch` can persist and still throw. Claiming
|
|
591
|
+
// either outcome would be a guess, so say so and point at the authoritative check.
|
|
592
|
+
if (!persisted && observed === undefined && commitAttempted) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
"Telegram notification settings may or may not have been saved, and the stored configuration could not be read; " +
|
|
595
|
+
`run \`gjc notify status\` before retrying: ${detail}`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
537
598
|
throw new Error(
|
|
538
|
-
|
|
599
|
+
persisted
|
|
539
600
|
? `Telegram notification settings were saved, but activation or recovery failed: ${detail}`
|
|
540
601
|
: `Unable to persist and activate Telegram notification settings: ${detail}`,
|
|
541
602
|
);
|
|
@@ -545,6 +606,26 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
545
606
|
);
|
|
546
607
|
}
|
|
547
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Whether the durable settings already carry the Telegram intent this setup run attempted to
|
|
611
|
+
* write: the same identity *and* the enabled state it would have produced. Matching the token
|
|
612
|
+
* and chat id alone is not enough — a previously disabled configuration can already hold both,
|
|
613
|
+
* and a commit that fails before enabling Telegram has persisted nothing new.
|
|
614
|
+
*
|
|
615
|
+
* Returns `undefined` when the durable state cannot be observed, so the caller can fall back
|
|
616
|
+
* instead of reporting a state nobody read.
|
|
617
|
+
*/
|
|
618
|
+
function telegramIntentIsPersisted(settings: Settings, botToken: string, chatId: string): boolean | undefined {
|
|
619
|
+
try {
|
|
620
|
+
const cfg = getNotificationConfig(settings);
|
|
621
|
+
return (
|
|
622
|
+
cfg.enabled === true && cfg.telegram?.enabled === true && cfg.botToken === botToken && cfg.chatId === chatId
|
|
623
|
+
);
|
|
624
|
+
} catch {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
548
629
|
function proposedIdentityFromSetupPreflight(
|
|
549
630
|
preflight: TelegramSetupPreflight,
|
|
550
631
|
botToken: string,
|
|
@@ -728,6 +809,143 @@ async function runRecovery(deps: NotifyCommandDeps, forceDaemonLock = false): Pr
|
|
|
728
809
|
process.stdout.write(`${formatNotificationRecoveryReport(report)}\n`);
|
|
729
810
|
}
|
|
730
811
|
|
|
812
|
+
/** Target and credential inputs stay owned by `notify setup`; binding never re-routes a session elsewhere. */
|
|
813
|
+
const BIND_THREAD_REJECTED_INPUTS: readonly (keyof NotifyCommandArgs)[] = [
|
|
814
|
+
"provider",
|
|
815
|
+
"token",
|
|
816
|
+
"chatId",
|
|
817
|
+
"discordBotToken",
|
|
818
|
+
"discordApplicationId",
|
|
819
|
+
"discordGuildId",
|
|
820
|
+
"discordParentChannelId",
|
|
821
|
+
"slackBotToken",
|
|
822
|
+
"slackAppToken",
|
|
823
|
+
"slackWorkspaceId",
|
|
824
|
+
"slackChannelId",
|
|
825
|
+
"slackAuthorizedUserId",
|
|
826
|
+
"message",
|
|
827
|
+
"probe",
|
|
828
|
+
"redact",
|
|
829
|
+
"forceDaemonLock",
|
|
830
|
+
"smoke",
|
|
831
|
+
];
|
|
832
|
+
|
|
833
|
+
export interface BindThreadInvocation {
|
|
834
|
+
sessionId: string;
|
|
835
|
+
threadTs: string;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Enforce the exact `bind-thread` grammar at every entrypoint.
|
|
840
|
+
*
|
|
841
|
+
* The command accepts only a session and a root; a positional argument, an
|
|
842
|
+
* unrelated notify flag, or a target/credential input is a rejection rather than
|
|
843
|
+
* something silently ignored, so no other invocation shape can reach the
|
|
844
|
+
* binding authority.
|
|
845
|
+
*/
|
|
846
|
+
export function assertStrictBindThreadInvocation(cmd: NotifyCommandArgs): BindThreadInvocation {
|
|
847
|
+
const rejected = BIND_THREAD_REJECTED_INPUTS.filter(key => {
|
|
848
|
+
const value = cmd[key];
|
|
849
|
+
return value !== undefined && value !== false && value !== "";
|
|
850
|
+
});
|
|
851
|
+
if (rejected.length > 0)
|
|
852
|
+
throw new Error(
|
|
853
|
+
`notify bind-thread accepts only --session-id and --thread-ts (rejected: ${rejected.join(", ")}).`,
|
|
854
|
+
);
|
|
855
|
+
const { sessionId, threadTs } = cmd;
|
|
856
|
+
if (!sessionId || !threadTs) throw new Error("notify bind-thread requires --session-id and --thread-ts.");
|
|
857
|
+
const allowed = new Set(["--session-id", sessionId, "--thread-ts", threadTs]);
|
|
858
|
+
const stray = cmd.rawArgs.filter(token => !allowed.has(token));
|
|
859
|
+
if (stray.length > 0)
|
|
860
|
+
throw new Error(`notify bind-thread does not accept additional arguments (rejected: ${stray.join(", ")}).`);
|
|
861
|
+
if (!isBoundedSlackRootTs(threadTs))
|
|
862
|
+
throw new SlackThreadBindingError(
|
|
863
|
+
"invalid_root",
|
|
864
|
+
"Slack root timestamp must be a bounded <seconds>.<fraction> message timestamp.",
|
|
865
|
+
);
|
|
866
|
+
return { sessionId, threadTs };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Adopt an existing Slack thread for a live session; the operator supplies only session and root identity. */
|
|
870
|
+
async function runBindThread(cmd: NotifyCommandArgs, deps: NotifyCommandDeps): Promise<void> {
|
|
871
|
+
const { sessionId, threadTs } = assertStrictBindThreadInvocation(cmd);
|
|
872
|
+
const bind = deps.bindSlackThread ?? (input => bindConfiguredSlackThread(input));
|
|
873
|
+
const bound = await bind({ settings: await getSettings(deps), sessionId, threadTs });
|
|
874
|
+
process.stdout.write(`${formatBoundSlackThread(bound)}\n`);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Confirmation carries identifiers only: no tokens, message bodies, or control secrets. */
|
|
878
|
+
export function formatBoundSlackThread(bound: BoundSlackThread): string {
|
|
879
|
+
return [
|
|
880
|
+
`${chalk.green("Bound")} Slack thread for session ${bound.sessionId}`,
|
|
881
|
+
` session generation: ${bound.endpointGeneration}`,
|
|
882
|
+
` workspace/channel: ${bound.teamId}/${bound.channelId}`,
|
|
883
|
+
` thread root: ${bound.rootTs}`,
|
|
884
|
+
` daemon owner: ${bound.ownerId} (generation ${bound.daemonGeneration})`,
|
|
885
|
+
].join("\n");
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** Activation carries only a session; a root or target here is a rejection, not an override. */
|
|
889
|
+
const ACTIVATE_THREAD_REJECTED_INPUTS: readonly (keyof NotifyCommandArgs)[] = [
|
|
890
|
+
...BIND_THREAD_REJECTED_INPUTS,
|
|
891
|
+
"threadTs",
|
|
892
|
+
];
|
|
893
|
+
|
|
894
|
+
export interface ActivateThreadInvocation {
|
|
895
|
+
sessionId: string;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Enforce the exact `activate-thread` grammar at every entrypoint.
|
|
900
|
+
*
|
|
901
|
+
* Activation names one prepared session and nothing else: the root it adopts is
|
|
902
|
+
* already the applied binding, so a supplied root, target, or credential is a
|
|
903
|
+
* rejection rather than something silently ignored.
|
|
904
|
+
*/
|
|
905
|
+
export function assertStrictActivateThreadInvocation(cmd: NotifyCommandArgs): ActivateThreadInvocation {
|
|
906
|
+
const rejected = ACTIVATE_THREAD_REJECTED_INPUTS.filter(key => {
|
|
907
|
+
const value = cmd[key];
|
|
908
|
+
return value !== undefined && value !== false && value !== "";
|
|
909
|
+
});
|
|
910
|
+
if (rejected.length > 0)
|
|
911
|
+
throw new Error(`notify activate-thread accepts only --session-id (rejected: ${rejected.join(", ")}).`);
|
|
912
|
+
const { sessionId } = cmd;
|
|
913
|
+
if (!sessionId) throw new Error("notify activate-thread requires --session-id.");
|
|
914
|
+
const allowed = new Set(["--session-id", sessionId]);
|
|
915
|
+
const stray = cmd.rawArgs.filter(token => !allowed.has(token));
|
|
916
|
+
if (stray.length > 0)
|
|
917
|
+
throw new Error(`notify activate-thread does not accept additional arguments (rejected: ${stray.join(", ")}).`);
|
|
918
|
+
return { sessionId };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Publish the readiness a prepared session withheld.
|
|
923
|
+
*
|
|
924
|
+
* The session's own host owns the decision: this command only proves discovery
|
|
925
|
+
* authority and asks it to activate, so activation before a binding exists is
|
|
926
|
+
* refused by the session rather than forced by the operator.
|
|
927
|
+
*/
|
|
928
|
+
async function runActivateThread(cmd: NotifyCommandArgs, deps: NotifyCommandDeps): Promise<void> {
|
|
929
|
+
const { sessionId } = assertStrictActivateThreadInvocation(cmd);
|
|
930
|
+
const activate =
|
|
931
|
+
deps.activatePreparedSession ??
|
|
932
|
+
(async (input: { settings: Settings; sessionId: string }) =>
|
|
933
|
+
await activatePreparedSession({
|
|
934
|
+
sessionIndex: await new SessionIndex(input.settings.getAgentDir()).open(),
|
|
935
|
+
sessionId: input.sessionId,
|
|
936
|
+
}));
|
|
937
|
+
const activated = await activate({ settings: await getSettings(deps), sessionId });
|
|
938
|
+
process.stdout.write(`${formatActivatedSession(activated)}\n`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Confirmation carries identifiers only: no endpoints, tokens, or thread content. */
|
|
942
|
+
export function formatActivatedSession(activated: ActivatedPreparedSession): string {
|
|
943
|
+
return [
|
|
944
|
+
`${chalk.green("Activated")} session ${activated.sessionId} (${activated.status})`,
|
|
945
|
+
` session generation: ${activated.endpointGeneration}`,
|
|
946
|
+
].join("\n");
|
|
947
|
+
}
|
|
948
|
+
|
|
731
949
|
export function printNotifyHelp(): void {
|
|
732
950
|
process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram, Discord, or Slack notifications
|
|
733
951
|
|
|
@@ -744,6 +962,8 @@ ${chalk.bold("Usage:")}
|
|
|
744
962
|
${APP_NAME} notify health [--provider telegram|discord|slack] [--probe]
|
|
745
963
|
${APP_NAME} notify test [--provider telegram|discord|slack] [--message <text>]
|
|
746
964
|
${APP_NAME} notify recovery [--force-daemon-lock]
|
|
965
|
+
${APP_NAME} notify bind-thread --session-id <sessionId> --thread-ts <rootTs>
|
|
966
|
+
${APP_NAME} notify activate-thread --session-id <sessionId>
|
|
747
967
|
|
|
748
968
|
${chalk.bold("Subcommands:")}
|
|
749
969
|
setup Pair Telegram or atomically save and activate complete Discord/Slack settings
|
|
@@ -751,6 +971,8 @@ ${chalk.bold("Subcommands:")}
|
|
|
751
971
|
health Report selected provider state; --probe uses REST only and never opens Gateway/Socket Mode
|
|
752
972
|
test Send a one-off test through one selected or uniquely effective provider
|
|
753
973
|
recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner); --force-daemon-lock retries only with the same fail-closed dead-owner proof
|
|
974
|
+
bind-thread Adopt an existing Slack thread as a live session's root; target and credentials come from setup only
|
|
975
|
+
activate-thread Publish the readiness a prepared session withheld once its thread binding is applied
|
|
754
976
|
|
|
755
977
|
${chalk.bold("Examples:")}
|
|
756
978
|
${APP_NAME} notify setup
|
|
@@ -761,6 +983,8 @@ ${chalk.bold("Examples:")}
|
|
|
761
983
|
${APP_NAME} notify health --provider discord --probe
|
|
762
984
|
${APP_NAME} notify test --provider slack --message "hello from gjc"
|
|
763
985
|
${APP_NAME} notify recovery
|
|
986
|
+
${APP_NAME} notify bind-thread --session-id 01J... --thread-ts 1785573662.132329
|
|
987
|
+
${APP_NAME} notify activate-thread --session-id 01J...
|
|
764
988
|
|
|
765
989
|
${chalk.bold("Threaded Mode:")}
|
|
766
990
|
GJC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
|
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") {
|