@gajae-code/coding-agent 0.11.5 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
- package/dist/types/session/session-manager.d.ts +10 -0
- package/dist/types/session/yield-queue.d.ts +8 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +1 -0
- package/src/config/settings-schema.ts +14 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +1 -1
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/runtime-init.ts +16 -2
- package/src/sdk/bus/chat-daemon-control.ts +19 -7
- package/src/sdk/bus/index.ts +4 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
- package/src/sdk/bus/telegram-daemon-control.ts +24 -6
- package/src/sdk/bus/telegram-daemon.ts +112 -2
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/session/agent-session.ts +972 -619
- package/src/session/session-manager.ts +66 -0
- package/src/session/yield-queue.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +36 -0
- package/src/tools/ask-answer-registry.ts +15 -1
|
@@ -865,6 +865,39 @@ function isExplicitlyStoppedDaemonState(state: unknown): state is DaemonState {
|
|
|
865
865
|
state.acquisitionId.length > 0,
|
|
866
866
|
);
|
|
867
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* v0.11.4 and earlier generations wrote stopped tombstones before acquisition
|
|
870
|
+
* and incarnation fields existed. A live, reused PID must not turn that durable
|
|
871
|
+
* stop marker into a foreign live owner during an upgrade.
|
|
872
|
+
*/
|
|
873
|
+
function isLegacyStoppedDaemonState(state: unknown): boolean {
|
|
874
|
+
const candidate = state as Partial<DaemonState> | undefined;
|
|
875
|
+
return Boolean(
|
|
876
|
+
candidate &&
|
|
877
|
+
Number.isSafeInteger(candidate.pid) &&
|
|
878
|
+
(candidate.pid ?? 0) > 0 &&
|
|
879
|
+
typeof candidate.ownerId === "string" &&
|
|
880
|
+
candidate.ownerId.length > 0 &&
|
|
881
|
+
typeof candidate.tokenFingerprint === "string" &&
|
|
882
|
+
typeof candidate.chatId === "string" &&
|
|
883
|
+
Number.isSafeInteger(candidate.startedAt) &&
|
|
884
|
+
Number.isSafeInteger(candidate.heartbeatAt) &&
|
|
885
|
+
Number.isSafeInteger(candidate.stoppedAt) &&
|
|
886
|
+
(candidate.launcherPid === undefined ||
|
|
887
|
+
(Number.isSafeInteger(candidate.launcherPid) && (candidate.launcherPid ?? 0) > 0)) &&
|
|
888
|
+
Array.isArray(candidate.roots) &&
|
|
889
|
+
candidate.roots.every(root => typeof root === "string") &&
|
|
890
|
+
candidate.version === DAEMON_VERSION &&
|
|
891
|
+
isRecognizedLegacyGeneration(candidate.generation) &&
|
|
892
|
+
candidate.incarnation === undefined &&
|
|
893
|
+
candidate.acquisitionId === undefined &&
|
|
894
|
+
candidate.ownershipPhase === undefined,
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function isStoppedDaemonState(state: unknown): boolean {
|
|
899
|
+
return isExplicitlyStoppedDaemonState(state) || isLegacyStoppedDaemonState(state);
|
|
900
|
+
}
|
|
868
901
|
|
|
869
902
|
function ownerIdentityMatches(
|
|
870
903
|
state: Pick<DaemonState, "tokenFingerprint" | "chatId">,
|
|
@@ -1016,7 +1049,7 @@ function classifyForeignLiveOwner(input: {
|
|
|
1016
1049
|
// Validate before probing so malformed PIDs never reach the liveness source.
|
|
1017
1050
|
if (!state || !validDaemonPid(state.pid) || !input.pidAlive(state.pid)) return undefined;
|
|
1018
1051
|
// A stopped tombstone with a canonical acquisition is not a live owner.
|
|
1019
|
-
if (
|
|
1052
|
+
if (isStoppedDaemonState(state) || isLegacyParentDaemonState(state)) return undefined;
|
|
1020
1053
|
const currentIncarnation = input.pidIncarnation?.(state.pid) ?? defaultPidIncarnation(state.pid);
|
|
1021
1054
|
if (
|
|
1022
1055
|
!hasSafeDaemonStateShape(state) ||
|
|
@@ -1156,7 +1189,7 @@ export async function acquireDaemonOwnership(input: {
|
|
|
1156
1189
|
if (state && !hasSafeDaemonStateShape(state)) {
|
|
1157
1190
|
const malformed = state as Partial<DaemonState>;
|
|
1158
1191
|
if (
|
|
1159
|
-
!
|
|
1192
|
+
!isStoppedDaemonState(malformed) &&
|
|
1160
1193
|
validDaemonPid(malformed.pid) &&
|
|
1161
1194
|
typeof malformed.incarnation === "string" &&
|
|
1162
1195
|
pidAlive(malformed.pid)
|
|
@@ -2010,6 +2043,60 @@ export async function spawnTelegramDaemonOwner(
|
|
|
2010
2043
|
return { result: "owner_spawned", acquisition, runtime, warnings: [] };
|
|
2011
2044
|
}
|
|
2012
2045
|
|
|
2046
|
+
/**
|
|
2047
|
+
* Owner-bound reclamation of a confirmed-dead daemon owner's lock, mirroring the
|
|
2048
|
+
* daemon step of `gjc notify recovery`. Removes only the lock of a recorded owner
|
|
2049
|
+
* that is still the same, still-dead process — never a live or superseded owner,
|
|
2050
|
+
* because it re-checks ownership and liveness while holding the same steal-mutex
|
|
2051
|
+
* the daemon's own takeover path uses. Lets a fresh SDK session self-heal a stale
|
|
2052
|
+
* owner (e.g. after a crash, or a `stop --force` that left the record behind)
|
|
2053
|
+
* instead of requiring manual reconciliation. Returns true when a stale lock was
|
|
2054
|
+
* cleared and a spawn retry is worthwhile.
|
|
2055
|
+
*/
|
|
2056
|
+
async function reclaimDeadDaemonOwner(input: {
|
|
2057
|
+
settings: Settings;
|
|
2058
|
+
fs?: TelegramDaemonFs;
|
|
2059
|
+
now?: () => number;
|
|
2060
|
+
pidAlive?: (pid: number) => boolean;
|
|
2061
|
+
pidIncarnation?: (pid: number) => string | undefined;
|
|
2062
|
+
}): Promise<boolean> {
|
|
2063
|
+
const fsImpl = input.fs ?? nodeFs;
|
|
2064
|
+
const now = input.now ?? Date.now;
|
|
2065
|
+
const pidAlive = input.pidAlive ?? defaultPidAlive;
|
|
2066
|
+
const pidIncarnation = input.pidIncarnation ?? defaultPidIncarnation;
|
|
2067
|
+
const paths = daemonPaths(input.settings.getAgentDir());
|
|
2068
|
+
const state = await readDaemonState(input.settings, fsImpl);
|
|
2069
|
+
if (!state || pidAlive(state.pid)) return false;
|
|
2070
|
+
const lock = await readOwnershipLock(fsImpl, paths.lock);
|
|
2071
|
+
if (lock.kind === "missing") return false;
|
|
2072
|
+
if (
|
|
2073
|
+
!(await ownershipLockIsReclaimable({ fs: fsImpl, path: paths.lock, lock, now: now(), pidAlive, pidIncarnation }))
|
|
2074
|
+
)
|
|
2075
|
+
return false;
|
|
2076
|
+
const transition = await acquireDaemonTransitionLock({
|
|
2077
|
+
fs: fsImpl,
|
|
2078
|
+
path: paths.steal,
|
|
2079
|
+
pid: process.pid,
|
|
2080
|
+
pidAlive: defaultPidAlive,
|
|
2081
|
+
pidIncarnation: defaultPidIncarnation,
|
|
2082
|
+
});
|
|
2083
|
+
if (!transition) return false;
|
|
2084
|
+
try {
|
|
2085
|
+
const current = await readDaemonState(input.settings, fsImpl);
|
|
2086
|
+
if (!current || current.ownerId !== state.ownerId || current.pid !== state.pid) return false;
|
|
2087
|
+
if (pidAlive(current.pid)) return false;
|
|
2088
|
+
if (!(await daemonTransitionLockIsHeld({ fs: fsImpl, path: paths.steal, lock: transition }))) return false;
|
|
2089
|
+
try {
|
|
2090
|
+
await fsImpl.unlink(paths.lock);
|
|
2091
|
+
return true;
|
|
2092
|
+
} catch {
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
} finally {
|
|
2096
|
+
await releaseDaemonTransitionLock({ fs: fsImpl, path: paths.steal, lock: transition });
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2013
2100
|
/**
|
|
2014
2101
|
* Ensure a configured daemon owns this session root, preserving ownership safety
|
|
2015
2102
|
* while exposing whether a #2028 generation handoff was required.
|
|
@@ -2063,6 +2150,29 @@ export async function ensureTelegramDaemonRunningDetailed(
|
|
|
2063
2150
|
);
|
|
2064
2151
|
}
|
|
2065
2152
|
}
|
|
2153
|
+
if (
|
|
2154
|
+
spawned.result === "blocked" &&
|
|
2155
|
+
!spawned.warnings[0]?.includes("provisional") &&
|
|
2156
|
+
(await reclaimDeadDaemonOwner({
|
|
2157
|
+
settings: input.settings,
|
|
2158
|
+
fs: deps.fs,
|
|
2159
|
+
now: deps.now,
|
|
2160
|
+
pidAlive: deps.pidAlive,
|
|
2161
|
+
pidIncarnation: deps.pidIncarnation,
|
|
2162
|
+
}))
|
|
2163
|
+
) {
|
|
2164
|
+
// A crashed owner (or a `stop --force` that found no live process) can leave a
|
|
2165
|
+
// stale lock behind that blocks re-acquisition. Automate the recovery step so a
|
|
2166
|
+
// fresh session self-heals instead of requiring a manual `gjc notify recovery`.
|
|
2167
|
+
spawned = await withTelegramSetupLease(
|
|
2168
|
+
cfg.botToken,
|
|
2169
|
+
async () =>
|
|
2170
|
+
await spawnTelegramDaemonOwner(
|
|
2171
|
+
{ settings: input.settings, roots: [root], tokenFingerprint: fp, chatId: cfg.chatId },
|
|
2172
|
+
deps,
|
|
2173
|
+
),
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2066
2176
|
if (spawned.result === "blocked") {
|
|
2067
2177
|
logger.warn(`notifications: failed to ensure Telegram daemon: ${spawned.warnings.join("; ")}`);
|
|
2068
2178
|
return "blocked_identity";
|
|
@@ -254,10 +254,17 @@ function invoke(
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
function errorResponse(id: string, row: Operation, error: unknown): ControlResponse {
|
|
257
|
-
const candidate = error as { code?: unknown; message?: unknown; recovery?: unknown };
|
|
257
|
+
const candidate = error as { code?: unknown; message?: unknown; recovery?: unknown; handoffDocument?: unknown };
|
|
258
258
|
const code = typeof candidate?.code === "string" ? candidate.code : undefined;
|
|
259
259
|
const message = typeof candidate?.message === "string" ? candidate.message : "Control operation failed.";
|
|
260
|
-
|
|
260
|
+
// A failed session.handoff is non-destructive and retains the generated
|
|
261
|
+
// document; surface it on the control protocol so external SDK/ACP/daemon
|
|
262
|
+
// clients can copy/retry it, mirroring the in-process seams and TUI.
|
|
263
|
+
const details: ControlValue | undefined =
|
|
264
|
+
row.sdkId === "session.handoff" && typeof candidate?.handoffDocument === "string"
|
|
265
|
+
? ({ handoffDocument: candidate.handoffDocument } as ControlValue)
|
|
266
|
+
: undefined;
|
|
267
|
+
if (error instanceof BusyError) return failure(id, "busy", message, undefined, details);
|
|
261
268
|
if (code === "default_model_selection_recovery" && row.errorCodes.includes(code)) {
|
|
262
269
|
const recovery = parseDefaultModelSelectionRecovery(candidate.recovery) ?? {
|
|
263
270
|
message: DEFAULT_MODEL_SELECTION_RECOVERY_MESSAGE,
|
|
@@ -265,11 +272,13 @@ function errorResponse(id: string, row: Operation, error: unknown): ControlRespo
|
|
|
265
272
|
};
|
|
266
273
|
return failure(id, code, DEFAULT_MODEL_SELECTION_RECOVERY_MESSAGE, undefined, recovery);
|
|
267
274
|
}
|
|
268
|
-
if (code && (row.errorCodes.includes(code) || SHARED_ERROR_CODES.has(code)))
|
|
275
|
+
if (code && (row.errorCodes.includes(code) || SHARED_ERROR_CODES.has(code)))
|
|
276
|
+
return failure(id, code, message, undefined, details);
|
|
269
277
|
if (code === "resource_gone" || /not found|gone/i.test(message)) return failure(id, "resource_gone", message);
|
|
270
278
|
if (code === "unknown_gate") return failure(id, "resource_gone", message);
|
|
271
|
-
if (code === "invalid_input" || /invalid input/i.test(message))
|
|
272
|
-
|
|
279
|
+
if (code === "invalid_input" || /invalid input/i.test(message))
|
|
280
|
+
return failure(id, "invalid_input", message, undefined, details);
|
|
281
|
+
return failure(id, "internal", "Control operation failed.", undefined, details);
|
|
273
282
|
}
|
|
274
283
|
|
|
275
284
|
async function execute(surface: ControlSurface, row: Operation, request: ControlRequest): Promise<ControlResponse> {
|
|
@@ -2065,6 +2065,24 @@
|
|
|
2065
2065
|
"packages/coding-agent/test/sdk-operation-inventory.test.ts"
|
|
2066
2066
|
]
|
|
2067
2067
|
},
|
|
2068
|
+
{
|
|
2069
|
+
"sourceId": "slash_command:handoff",
|
|
2070
|
+
"sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
|
|
2071
|
+
"sourceKind": "slash_command",
|
|
2072
|
+
"decision": "include",
|
|
2073
|
+
"sdkId": "session.handoff",
|
|
2074
|
+
"adapterMappings": {
|
|
2075
|
+
"telegram": "generic_safe",
|
|
2076
|
+
"discord": "generic_safe",
|
|
2077
|
+
"slack": "generic_safe",
|
|
2078
|
+
"mcp": "generic_safe",
|
|
2079
|
+
"acp": "generic_safe",
|
|
2080
|
+
"daemonCli": "generic_safe"
|
|
2081
|
+
},
|
|
2082
|
+
"testIds": [
|
|
2083
|
+
"packages/coding-agent/test/sdk-operation-inventory.test.ts"
|
|
2084
|
+
]
|
|
2085
|
+
},
|
|
2068
2086
|
{
|
|
2069
2087
|
"sourceId": "slash_command:contribute-pr",
|
|
2070
2088
|
"sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
|