@gajae-code/coding-agent 0.11.0 → 0.11.1
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 +9 -1
- package/dist/types/async/job-manager.d.ts +46 -7
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +1 -0
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/sdk/broker/ensure.d.ts +9 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -0
- package/dist/types/tools/subagent-render.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +6 -1
- package/package.json +8 -8
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/src/async/job-manager.ts +406 -63
- package/src/gjc-runtime/ultragoal-guard.ts +55 -8
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +72 -8
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/controllers/command-controller.ts +9 -15
- package/src/modes/controllers/extension-ui-controller.ts +48 -24
- package/src/modes/interactive-mode.ts +28 -16
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +7 -3
- package/src/modes/types.ts +2 -2
- package/src/sdk/broker/discovery.ts +20 -1
- package/src/sdk/broker/ensure.ts +82 -24
- package/src/sdk/bus/chat-effect-journal.ts +6 -4
- package/src/sdk/bus/discord-daemon.ts +25 -8
- package/src/sdk/bus/index.ts +57 -21
- package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +102 -10
- package/src/session/agent-session.ts +176 -29
- package/src/task/index.ts +26 -16
- package/src/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +84 -32
package/src/sdk/bus/index.ts
CHANGED
|
@@ -874,6 +874,10 @@ interface SessionRuntime {
|
|
|
874
874
|
redact: boolean;
|
|
875
875
|
/** True only after the exact host generation was registered with the broker index. */
|
|
876
876
|
brokerRegistrationActive: boolean;
|
|
877
|
+
/** Terminal cleanup proof retained across retries; each owner is released at most once after proof. */
|
|
878
|
+
hostStopped: boolean;
|
|
879
|
+
serverStopped: boolean;
|
|
880
|
+
brokerRegistrationReleased: boolean;
|
|
877
881
|
verbosity: "lean" | "verbose";
|
|
878
882
|
sessionTag: string;
|
|
879
883
|
/** Whether the agent loop is currently running (drives the typing indicator). */
|
|
@@ -2294,6 +2298,9 @@ export function createNotificationsExtension(
|
|
|
2294
2298
|
getConfig: () => resolveSettings(options.settings).cfg,
|
|
2295
2299
|
});
|
|
2296
2300
|
|
|
2301
|
+
// Failed terminal teardown remains fenced from normal runtime lookup while the
|
|
2302
|
+
// exact runtime object retains authority for an explicit idempotent retry.
|
|
2303
|
+
const cleanupRetries = new Map<string, SessionRuntime>();
|
|
2297
2304
|
const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
|
|
2298
2305
|
let activeRuntimeId: string | undefined;
|
|
2299
2306
|
let identityControlInFlight = false;
|
|
@@ -2341,13 +2348,15 @@ export function createNotificationsExtension(
|
|
|
2341
2348
|
reason: "session" | "notifications" = "session",
|
|
2342
2349
|
expectedRuntime?: SessionRuntime,
|
|
2343
2350
|
): Promise<boolean> {
|
|
2344
|
-
const
|
|
2351
|
+
const retryRuntime = cleanupRetries.get(id);
|
|
2352
|
+
const activeRuntime = runtimes.get(id);
|
|
2353
|
+
const requestedRuntime = retryRuntime ?? activeRuntime;
|
|
2345
2354
|
if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
|
|
2346
2355
|
if (reason === "session" && requestedRuntime) requestedRuntime.stopping = true;
|
|
2347
2356
|
if (reason === "session" && requestedRuntime) {
|
|
2348
2357
|
// Fence the exact runtime before awaiting its startup promise: a late start
|
|
2349
2358
|
// must observe removal and clean itself up rather than becoming reachable.
|
|
2350
|
-
runtimes.delete(id);
|
|
2359
|
+
if (runtimes.get(id) === requestedRuntime) runtimes.delete(id);
|
|
2351
2360
|
if (activeRuntimeId === id) activeRuntimeId = undefined;
|
|
2352
2361
|
}
|
|
2353
2362
|
const pendingStart = sessionStartPromises.get(id);
|
|
@@ -2355,7 +2364,8 @@ export function createNotificationsExtension(
|
|
|
2355
2364
|
void pendingStart
|
|
2356
2365
|
.catch(() => {})
|
|
2357
2366
|
.then(() => {
|
|
2358
|
-
if (runtimes.get(id) === requestedRuntime
|
|
2367
|
+
if (runtimes.get(id) === requestedRuntime || cleanupRetries.get(id) === requestedRuntime)
|
|
2368
|
+
void stopSession(id, reason, requestedRuntime);
|
|
2359
2369
|
});
|
|
2360
2370
|
const rt = requestedRuntime;
|
|
2361
2371
|
|
|
@@ -2378,6 +2388,9 @@ export function createNotificationsExtension(
|
|
|
2378
2388
|
rt.pendingInteractive.clear();
|
|
2379
2389
|
return true;
|
|
2380
2390
|
}
|
|
2391
|
+
// Keep this exact object authoritative for the full terminal release, including
|
|
2392
|
+
// the interval before a failed owner can be recorded for a later retry.
|
|
2393
|
+
cleanupRetries.set(id, rt);
|
|
2381
2394
|
|
|
2382
2395
|
try {
|
|
2383
2396
|
rt.cancelPostmortemCleanup();
|
|
@@ -2405,16 +2418,24 @@ export function createNotificationsExtension(
|
|
|
2405
2418
|
try {
|
|
2406
2419
|
rt.disposeGateTerminalController();
|
|
2407
2420
|
} catch {}
|
|
2408
|
-
let hostStopped =
|
|
2409
|
-
let brokerRegistrationReleased =
|
|
2421
|
+
let hostStopped = rt.hostStopped;
|
|
2422
|
+
let brokerRegistrationReleased = rt.brokerRegistrationReleased;
|
|
2423
|
+
const ownerReleaseFailures: unknown[] = [];
|
|
2410
2424
|
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2425
|
+
if (!hostStopped) {
|
|
2426
|
+
try {
|
|
2427
|
+
const stopped = await rt.host.stop();
|
|
2428
|
+
hostStopped = stopped === "stopped";
|
|
2429
|
+
brokerRegistrationReleased = !rt.brokerRegistrationActive || hostStopped;
|
|
2430
|
+
if (rt.brokerRegistrationActive && hostStopped) rt.brokerRegistrationActive = false;
|
|
2431
|
+
if (hostStopped) {
|
|
2432
|
+
rt.hostStopped = true;
|
|
2433
|
+
rt.brokerRegistrationReleased = brokerRegistrationReleased;
|
|
2434
|
+
}
|
|
2435
|
+
} catch (e) {
|
|
2436
|
+
ownerReleaseFailures.push(e);
|
|
2437
|
+
logger.warn(`sdk host: stop failed: ${String(e)}`);
|
|
2438
|
+
}
|
|
2418
2439
|
}
|
|
2419
2440
|
rt.host.reverse.dispose();
|
|
2420
2441
|
// Resolve any still-pending interactive asks so the ask tool is not left hanging.
|
|
@@ -2424,6 +2445,7 @@ export function createNotificationsExtension(
|
|
|
2424
2445
|
rt.cursors.close();
|
|
2425
2446
|
await rt.revisions.close();
|
|
2426
2447
|
} catch (e) {
|
|
2448
|
+
ownerReleaseFailures.push(e);
|
|
2427
2449
|
logger.warn(`sdk query snapshots: close failed: ${String(e)}`);
|
|
2428
2450
|
}
|
|
2429
2451
|
let closeFrameSent = false;
|
|
@@ -2434,18 +2456,27 @@ export function createNotificationsExtension(
|
|
|
2434
2456
|
logger.warn(`notifications: session_closed failed: ${String(e)}`);
|
|
2435
2457
|
}
|
|
2436
2458
|
if (closeFrameSent) await sleep(100);
|
|
2437
|
-
let serverStopped =
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2459
|
+
let serverStopped = rt.serverStopped;
|
|
2460
|
+
if (!serverStopped) {
|
|
2461
|
+
try {
|
|
2462
|
+
await rt.server.stopAndWait();
|
|
2463
|
+
serverStopped = true;
|
|
2464
|
+
rt.serverStopped = true;
|
|
2465
|
+
} catch (e) {
|
|
2466
|
+
ownerReleaseFailures.push(e);
|
|
2467
|
+
logger.warn(`notifications: stop failed: ${String(e)}`);
|
|
2468
|
+
}
|
|
2443
2469
|
}
|
|
2444
2470
|
lifecycleStartupCapability?.rollback?.recordStop(rt.host.generation, {
|
|
2445
2471
|
runtimeRemoved: true,
|
|
2446
|
-
hostStopped: hostStopped && serverStopped,
|
|
2447
|
-
brokerRegistrationReleased,
|
|
2472
|
+
hostStopped: rt.hostStopped && rt.serverStopped,
|
|
2473
|
+
brokerRegistrationReleased: rt.brokerRegistrationReleased,
|
|
2448
2474
|
});
|
|
2475
|
+
if (ownerReleaseFailures.length > 0) {
|
|
2476
|
+
cleanupRetries.set(id, rt);
|
|
2477
|
+
throw new AggregateError(ownerReleaseFailures, `SDK notification runtime ${id} owner release failed.`);
|
|
2478
|
+
}
|
|
2479
|
+
if (cleanupRetries.get(id) === rt) cleanupRetries.delete(id);
|
|
2449
2480
|
return true;
|
|
2450
2481
|
}
|
|
2451
2482
|
|
|
@@ -2484,6 +2515,8 @@ export function createNotificationsExtension(
|
|
|
2484
2515
|
if (lifecycleRequired && !isNotificationEligibleContext(ctx)) return failLifecycleStartup("ineligible");
|
|
2485
2516
|
const pendingStart = sessionStartPromises.get(id);
|
|
2486
2517
|
if (pendingStart) return pendingStart;
|
|
2518
|
+
const retainedCleanup = cleanupRetries.get(id);
|
|
2519
|
+
if (retainedCleanup) return failLifecycleStartup("failed", "SDK notification runtime cleanup is still pending.");
|
|
2487
2520
|
const existingRuntime = runtimes.get(id);
|
|
2488
2521
|
if (existingRuntime) {
|
|
2489
2522
|
activeRuntimeId = id;
|
|
@@ -2930,6 +2963,9 @@ export function createNotificationsExtension(
|
|
|
2930
2963
|
idleSeq: 0,
|
|
2931
2964
|
pendingInteractive,
|
|
2932
2965
|
brokerRegistrationActive: false,
|
|
2966
|
+
hostStopped: false,
|
|
2967
|
+
serverStopped: false,
|
|
2968
|
+
brokerRegistrationReleased: false,
|
|
2933
2969
|
disposeAnswerSource: () => {},
|
|
2934
2970
|
disposeFileSink: () => {},
|
|
2935
2971
|
disposeGateListener: () => {},
|
|
@@ -2980,7 +3016,7 @@ export function createNotificationsExtension(
|
|
|
2980
3016
|
};
|
|
2981
3017
|
const cleanupAbandonedStartup = async (): Promise<void> => {
|
|
2982
3018
|
try {
|
|
2983
|
-
server.
|
|
3019
|
+
await server.stopAndWait();
|
|
2984
3020
|
} catch {}
|
|
2985
3021
|
try {
|
|
2986
3022
|
await host.stop();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const REQUIRED_WORKFLOW_ARBITRATION_METHODS = ["registerArbitratedAsk", "retireIfUnclaimed"] as const;
|
|
1
|
+
const REQUIRED_WORKFLOW_ARBITRATION_METHODS = ["registerArbitratedAsk", "retireIfUnclaimed", "stopAndWait"] as const;
|
|
2
2
|
|
|
3
3
|
export class NativeRuntimeCompatibilityError extends Error {
|
|
4
4
|
readonly code = "native_runtime_incompatible";
|
|
@@ -941,23 +941,96 @@ type PairedChatPrivacy = "private" | "non-private" | "indeterminate";
|
|
|
941
941
|
|
|
942
942
|
export type TelegramUpdateOutcome = "consumed" | "retry";
|
|
943
943
|
|
|
944
|
+
export type TelegramPollResult =
|
|
945
|
+
| { kind: "success"; updateCount: number }
|
|
946
|
+
| { kind: "aborted" }
|
|
947
|
+
| { kind: "getUpdates_failed"; error: string }
|
|
948
|
+
| { kind: "api_failure"; errorCode?: number; description: string }
|
|
949
|
+
| { kind: "conflict"; description: string; backoffMs: number };
|
|
950
|
+
|
|
944
951
|
export interface TelegramUpdatePollerOptions {
|
|
945
952
|
botApi: BotApi;
|
|
946
953
|
runtime: NotificationOperatorRuntime;
|
|
947
954
|
backoff: OperatorBackoffPolicy;
|
|
948
955
|
processUpdate: (update: unknown) => Promise<TelegramUpdateOutcome>;
|
|
956
|
+
health?: TelegramPollHealth;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
type TelegramPollHealthStatus = "healthy" | "getUpdates_failed" | "api_failure" | "conflict";
|
|
960
|
+
|
|
961
|
+
export class TelegramPollHealth {
|
|
962
|
+
#status: TelegramPollHealthStatus = "healthy";
|
|
963
|
+
#suppressedCount = 0;
|
|
964
|
+
|
|
965
|
+
record(result: TelegramPollResult): void {
|
|
966
|
+
if (result.kind === "aborted") return;
|
|
967
|
+
if (result.kind === "success") {
|
|
968
|
+
if (this.#status !== "healthy") {
|
|
969
|
+
logger.info("notifications daemon: Telegram getUpdates recovered", {
|
|
970
|
+
from: this.#status,
|
|
971
|
+
suppressedCount: this.#suppressedCount,
|
|
972
|
+
updateCount: result.updateCount,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
this.#status = "healthy";
|
|
976
|
+
this.#suppressedCount = 0;
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const previousStatus = this.#status;
|
|
981
|
+
if (previousStatus === result.kind) {
|
|
982
|
+
this.#suppressedCount += 1;
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const suppressedCount = this.#suppressedCount;
|
|
987
|
+
this.#status = result.kind;
|
|
988
|
+
this.#suppressedCount = 0;
|
|
989
|
+
if (result.kind === "conflict") {
|
|
990
|
+
logger.error("notifications daemon: Telegram getUpdates 409 conflict", {
|
|
991
|
+
description: result.description,
|
|
992
|
+
backoffMs: result.backoffMs,
|
|
993
|
+
previousStatus,
|
|
994
|
+
suppressedCount,
|
|
995
|
+
});
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (result.kind === "api_failure") {
|
|
999
|
+
logger.error("notifications daemon: Telegram getUpdates API failed", {
|
|
1000
|
+
errorCode: result.errorCode,
|
|
1001
|
+
description: result.description,
|
|
1002
|
+
previousStatus,
|
|
1003
|
+
suppressedCount,
|
|
1004
|
+
});
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
logger.error("notifications daemon: getUpdates failed", {
|
|
1009
|
+
error: result.error,
|
|
1010
|
+
previousStatus,
|
|
1011
|
+
suppressedCount,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
949
1014
|
}
|
|
950
1015
|
|
|
951
1016
|
/** Owns getUpdates offset, conflict backoff, and per-update error isolation. */
|
|
952
1017
|
export class TelegramUpdatePoller {
|
|
953
1018
|
#offset = 0;
|
|
954
1019
|
#opts: TelegramUpdatePollerOptions;
|
|
1020
|
+
#health: TelegramPollHealth;
|
|
955
1021
|
|
|
956
1022
|
constructor(opts: TelegramUpdatePollerOptions) {
|
|
957
1023
|
this.#opts = opts;
|
|
1024
|
+
this.#health = opts.health ?? new TelegramPollHealth();
|
|
958
1025
|
}
|
|
959
1026
|
|
|
960
1027
|
async pollOnce(signal?: AbortSignal): Promise<number> {
|
|
1028
|
+
const result = await this.pollOnceResult(signal);
|
|
1029
|
+
this.#health.record(result);
|
|
1030
|
+
return result.kind === "success" ? result.updateCount : 0;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async pollOnceResult(signal?: AbortSignal): Promise<TelegramPollResult> {
|
|
961
1034
|
let body: {
|
|
962
1035
|
ok?: boolean;
|
|
963
1036
|
error_code?: number;
|
|
@@ -972,24 +1045,37 @@ export class TelegramUpdatePoller {
|
|
|
972
1045
|
)) as typeof body;
|
|
973
1046
|
} catch (err) {
|
|
974
1047
|
// A cooperative stop aborts the in-flight long poll; treat as a clean wake.
|
|
975
|
-
if (isAbortError(err)) return
|
|
1048
|
+
if (isAbortError(err)) return { kind: "aborted" };
|
|
976
1049
|
// A transient Telegram API failure must never crash the daemon.
|
|
977
|
-
logger.error("notifications daemon: getUpdates failed", { error: sanitizeDiagnostic(String(err)) });
|
|
978
1050
|
await this.#opts.runtime.sleep(POLL_BACKOFF_MS, signal);
|
|
979
|
-
return
|
|
1051
|
+
return { kind: "getUpdates_failed", error: sanitizeDiagnostic(String(err)) };
|
|
980
1052
|
}
|
|
981
1053
|
// Telegram allows only one active getUpdates poller per bot. A 409 means
|
|
982
1054
|
// another poller is live; back off boundedly instead of hot-looping.
|
|
983
1055
|
if (body && body.ok === false && (body.error_code === 409 || /409|conflict/i.test(body.description ?? ""))) {
|
|
984
1056
|
const backoffMs = this.#opts.backoff.next();
|
|
985
|
-
logger.error(
|
|
986
|
-
`notifications daemon: Telegram getUpdates 409 conflict (${body.description ?? "no description"}); backing off ${backoffMs}ms`,
|
|
987
|
-
);
|
|
988
1057
|
await this.#opts.runtime.sleep(backoffMs, signal);
|
|
989
|
-
return
|
|
1058
|
+
return { kind: "conflict", description: sanitizeDiagnostic(body.description ?? "no description"), backoffMs };
|
|
1059
|
+
}
|
|
1060
|
+
if (body?.ok !== true || !Array.isArray(body.result)) {
|
|
1061
|
+
await this.#opts.runtime.sleep(POLL_BACKOFF_MS, signal);
|
|
1062
|
+
return {
|
|
1063
|
+
kind: "api_failure",
|
|
1064
|
+
errorCode: typeof body?.error_code === "number" ? body.error_code : undefined,
|
|
1065
|
+
description: sanitizeDiagnostic(body?.description ?? "Malformed getUpdates response"),
|
|
1066
|
+
};
|
|
990
1067
|
}
|
|
991
1068
|
this.#opts.backoff.reset();
|
|
992
|
-
|
|
1069
|
+
let malformedSeen = false;
|
|
1070
|
+
for (const update of body.result) {
|
|
1071
|
+
// A single malformed update_id must not wedge the poller. Skip the
|
|
1072
|
+
// bad entry (surfaced below as an api_failure health signal) while
|
|
1073
|
+
// still processing valid updates and advancing the offset past them,
|
|
1074
|
+
// so one poisoned item cannot stall an otherwise-valid stream.
|
|
1075
|
+
if (!Number.isSafeInteger(update?.update_id)) {
|
|
1076
|
+
malformedSeen = true;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
993
1079
|
try {
|
|
994
1080
|
const outcome = await this.#opts.processUpdate(update);
|
|
995
1081
|
if (outcome === "retry") {
|
|
@@ -998,11 +1084,17 @@ export class TelegramUpdatePoller {
|
|
|
998
1084
|
}
|
|
999
1085
|
this.#offset = update.update_id + 1;
|
|
1000
1086
|
} catch (err) {
|
|
1001
|
-
logger.error("notifications daemon: handleTelegramUpdate failed", {
|
|
1087
|
+
logger.error("notifications daemon: handleTelegramUpdate failed", {
|
|
1088
|
+
error: sanitizeDiagnostic(String(err)),
|
|
1089
|
+
});
|
|
1002
1090
|
this.#offset = update.update_id + 1;
|
|
1003
1091
|
}
|
|
1004
1092
|
}
|
|
1005
|
-
|
|
1093
|
+
if (malformedSeen) {
|
|
1094
|
+
await this.#opts.runtime.sleep(POLL_BACKOFF_MS, signal);
|
|
1095
|
+
return { kind: "api_failure", description: "Malformed getUpdates response" };
|
|
1096
|
+
}
|
|
1097
|
+
return { kind: "success", updateCount: body.result.length };
|
|
1006
1098
|
}
|
|
1007
1099
|
}
|
|
1008
1100
|
|
|
@@ -57,6 +57,8 @@ import {
|
|
|
57
57
|
generateHandoff,
|
|
58
58
|
IMAGE_TOKEN_ESTIMATE,
|
|
59
59
|
prepareCompaction,
|
|
60
|
+
type RemoteCompactionFallbackHealthEvent,
|
|
61
|
+
type RemoteCompactionFallbackHealthHooks,
|
|
60
62
|
type SummaryOptions,
|
|
61
63
|
shouldCompact,
|
|
62
64
|
} from "@gajae-code/agent-core/compaction";
|
|
@@ -641,6 +643,40 @@ interface TemporaryProviderSessionScopeRecord {
|
|
|
641
643
|
providerSessionState: Map<string, ProviderSessionState>;
|
|
642
644
|
}
|
|
643
645
|
|
|
646
|
+
class RemoteCompactionFallbackHealth implements RemoteCompactionFallbackHealthHooks {
|
|
647
|
+
#status: "healthy" | "fallback" = "healthy";
|
|
648
|
+
#suppressedCount = 0;
|
|
649
|
+
|
|
650
|
+
recordRemoteCompactionFallback(event: RemoteCompactionFallbackHealthEvent): void {
|
|
651
|
+
if (event.kind === "success") {
|
|
652
|
+
if (this.#status === "fallback") {
|
|
653
|
+
logger.info("OpenAI remote compaction recovered", {
|
|
654
|
+
model: event.model,
|
|
655
|
+
provider: event.provider,
|
|
656
|
+
suppressedCount: this.#suppressedCount,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
this.#status = "healthy";
|
|
660
|
+
this.#suppressedCount = 0;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (this.#status === "fallback") {
|
|
665
|
+
this.#suppressedCount += 1;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
this.#status = "fallback";
|
|
670
|
+
this.#suppressedCount = 0;
|
|
671
|
+
logger.warn("OpenAI remote compaction failed, falling back to local summarization", {
|
|
672
|
+
error: event.error,
|
|
673
|
+
model: event.model,
|
|
674
|
+
provider: event.provider,
|
|
675
|
+
suppressedCount: 0,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
644
680
|
/** Result from cycleRoleModels() */
|
|
645
681
|
export interface RoleModelCycleResult {
|
|
646
682
|
model: Model;
|
|
@@ -1419,6 +1455,7 @@ export class AgentSession {
|
|
|
1419
1455
|
#providerCacheSessionId: string | undefined;
|
|
1420
1456
|
#isDisposed = false;
|
|
1421
1457
|
#disposePromise: Promise<void> | undefined;
|
|
1458
|
+
#newSessionTransition: Promise<boolean> | undefined;
|
|
1422
1459
|
// Extension system
|
|
1423
1460
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
1424
1461
|
|
|
@@ -1544,6 +1581,7 @@ export class AgentSession {
|
|
|
1544
1581
|
}
|
|
1545
1582
|
>();
|
|
1546
1583
|
|
|
1584
|
+
#remoteCompactionFallbackHealth = new RemoteCompactionFallbackHealth();
|
|
1547
1585
|
#streamingEditPrecheckedToolCallIds = new Set<string>();
|
|
1548
1586
|
|
|
1549
1587
|
#streamingEditFileCache = new StreamingEditFileCache();
|
|
@@ -7621,7 +7659,20 @@ export class AgentSession {
|
|
|
7621
7659
|
* @param options - Optional initial messages and parent session path
|
|
7622
7660
|
* @returns true if completed, false if cancelled by hook
|
|
7623
7661
|
*/
|
|
7624
|
-
|
|
7662
|
+
newSession(options?: NewSessionOptions): Promise<boolean> {
|
|
7663
|
+
if (this.#newSessionTransition) return this.#newSessionTransition;
|
|
7664
|
+
|
|
7665
|
+
const transition = this.#runNewSessionTransition(options);
|
|
7666
|
+
this.#newSessionTransition = transition;
|
|
7667
|
+
void transition
|
|
7668
|
+
.finally(() => {
|
|
7669
|
+
if (this.#newSessionTransition === transition) this.#newSessionTransition = undefined;
|
|
7670
|
+
})
|
|
7671
|
+
.catch(() => {});
|
|
7672
|
+
return transition;
|
|
7673
|
+
}
|
|
7674
|
+
|
|
7675
|
+
async #runNewSessionTransition(options?: NewSessionOptions): Promise<boolean> {
|
|
7625
7676
|
const previousSessionFile = this.sessionFile;
|
|
7626
7677
|
const previousWorkflowGateSessionId = this.sessionId;
|
|
7627
7678
|
const nextDiscoverySessionToolNames = this.#mcpDiscoveryEnabled
|
|
@@ -7643,32 +7694,132 @@ export class AgentSession {
|
|
|
7643
7694
|
}
|
|
7644
7695
|
}
|
|
7645
7696
|
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7697
|
+
const manager = AsyncJobManager.instance();
|
|
7698
|
+
const ownerId = this.#agentId;
|
|
7699
|
+
const lease = manager && ownerId ? manager.beginOwnerSubagentShutdown(ownerId) : undefined;
|
|
7700
|
+
if (manager && ownerId && !lease) {
|
|
7701
|
+
this.emitNotice(
|
|
7702
|
+
"error",
|
|
7703
|
+
"Cannot start a new session while owned subagent cleanup is already in progress.",
|
|
7704
|
+
"new-session-subagent-cleanup",
|
|
7705
|
+
);
|
|
7706
|
+
return false;
|
|
7707
|
+
}
|
|
7708
|
+
|
|
7709
|
+
if (!lease) {
|
|
7710
|
+
this.#disconnectFromAgent();
|
|
7711
|
+
await this.abort();
|
|
7712
|
+
if (this.isCompacting) {
|
|
7713
|
+
this.abortCompaction();
|
|
7714
|
+
while (this.isCompacting) {
|
|
7715
|
+
await Bun.sleep(10);
|
|
7716
|
+
}
|
|
7717
|
+
}
|
|
7718
|
+
this.#cancelOwnAsyncJobs();
|
|
7719
|
+
this.#closeAllProviderSessions("new session");
|
|
7720
|
+
this.#rebindProviderSessionState(new Map());
|
|
7721
|
+
this.agent.reset();
|
|
7722
|
+
if (!options?.drop) await this.sessionManager.flush();
|
|
7723
|
+
await this.sessionManager.newSession(options);
|
|
7724
|
+
this.setTodoPhases([]);
|
|
7725
|
+
this.#syncAgentSessionId();
|
|
7726
|
+
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
7727
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
7728
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
7729
|
+
this.#steeringMessages = [];
|
|
7730
|
+
this.#followUpMessages = [];
|
|
7731
|
+
this.#pendingNextTurnMessages = [];
|
|
7732
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7733
|
+
await this.#initializeNewSessionState(nextDiscoverySessionToolNames, previousSessionFile);
|
|
7734
|
+
if (options?.drop && previousSessionFile) {
|
|
7735
|
+
try {
|
|
7736
|
+
await this.sessionManager.dropSession(previousSessionFile);
|
|
7737
|
+
} catch (err) {
|
|
7738
|
+
logger.error("Failed to delete session during /drop", { err });
|
|
7739
|
+
}
|
|
7740
|
+
}
|
|
7741
|
+
return true;
|
|
7742
|
+
}
|
|
7743
|
+
|
|
7744
|
+
if (!manager) throw new Error("Owner subagent shutdown manager became unavailable.");
|
|
7745
|
+
if (!ownerId) throw new Error("Owner subagent shutdown owner became unavailable.");
|
|
7746
|
+
const previousSessionIdentity = this.sessionManager.getSessionId();
|
|
7747
|
+
try {
|
|
7653
7748
|
try {
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7749
|
+
manager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
7750
|
+
await this.abort();
|
|
7751
|
+
if (this.isCompacting) {
|
|
7752
|
+
this.abortCompaction();
|
|
7753
|
+
while (this.isCompacting) {
|
|
7754
|
+
await Bun.sleep(10);
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
const proof = await manager.cancelAndProveOwnerSubagents(lease);
|
|
7758
|
+
if (!proof.confirmed) {
|
|
7759
|
+
this.emitNotice(
|
|
7760
|
+
"error",
|
|
7761
|
+
"Unable to confirm owned subagent cleanup; session was not replaced. Wait for or inspect remaining subagents, then retry /new.",
|
|
7762
|
+
"new-session-subagent-cleanup",
|
|
7763
|
+
);
|
|
7764
|
+
manager.finishOwnerSubagentShutdown(lease, "release");
|
|
7765
|
+
return false;
|
|
7766
|
+
}
|
|
7767
|
+
} catch {
|
|
7768
|
+
this.emitNotice(
|
|
7769
|
+
"error",
|
|
7770
|
+
"Unable to confirm owned subagent cleanup; session was not replaced. Wait for or inspect remaining subagents, then retry /new.",
|
|
7771
|
+
"new-session-subagent-cleanup",
|
|
7772
|
+
);
|
|
7773
|
+
manager.finishOwnerSubagentShutdown(lease, "release");
|
|
7774
|
+
return false;
|
|
7657
7775
|
}
|
|
7658
|
-
|
|
7659
|
-
await
|
|
7776
|
+
|
|
7777
|
+
if (!(await manager.waitForOwnerInFlightDeliveries(ownerId))) {
|
|
7778
|
+
throw new Error("Owned async deliveries did not settle before session replacement.");
|
|
7779
|
+
}
|
|
7780
|
+
if (!options?.drop) await this.sessionManager.flush();
|
|
7781
|
+
|
|
7782
|
+
if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) {
|
|
7783
|
+
throw new Error("Owned async jobs did not settle before session replacement.");
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
this.#disconnectFromAgent();
|
|
7787
|
+
this.#closeAllProviderSessions("new session");
|
|
7788
|
+
this.#rebindProviderSessionState(new Map());
|
|
7789
|
+
this.agent.reset();
|
|
7790
|
+
await this.sessionManager.newSession(options);
|
|
7791
|
+
this.setTodoPhases([]);
|
|
7792
|
+
this.#syncAgentSessionId();
|
|
7793
|
+
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
7794
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
7795
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
7796
|
+
this.#steeringMessages = [];
|
|
7797
|
+
this.#followUpMessages = [];
|
|
7798
|
+
this.#pendingNextTurnMessages = [];
|
|
7799
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7800
|
+
await this.#initializeNewSessionState(nextDiscoverySessionToolNames, previousSessionFile);
|
|
7801
|
+
if (options?.drop && previousSessionFile) {
|
|
7802
|
+
try {
|
|
7803
|
+
await this.sessionManager.dropSession(previousSessionFile);
|
|
7804
|
+
} catch (err) {
|
|
7805
|
+
logger.error("Failed to delete session during /drop", { err });
|
|
7806
|
+
}
|
|
7807
|
+
}
|
|
7808
|
+
manager.finishOwnerSubagentShutdown(lease, "commit");
|
|
7809
|
+
return true;
|
|
7810
|
+
} catch (error) {
|
|
7811
|
+
manager.finishOwnerSubagentShutdown(
|
|
7812
|
+
lease,
|
|
7813
|
+
this.sessionManager.getSessionId() !== previousSessionIdentity ? "commit" : "release",
|
|
7814
|
+
);
|
|
7815
|
+
throw error;
|
|
7660
7816
|
}
|
|
7661
|
-
|
|
7662
|
-
this.setTodoPhases([]);
|
|
7663
|
-
this.#syncAgentSessionId();
|
|
7664
|
-
this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
|
|
7665
|
-
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
7666
|
-
this.#resetHindsightConversationTrackingIfHindsight();
|
|
7667
|
-
this.#steeringMessages = [];
|
|
7668
|
-
this.#followUpMessages = [];
|
|
7669
|
-
this.#pendingNextTurnMessages = [];
|
|
7670
|
-
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7817
|
+
}
|
|
7671
7818
|
|
|
7819
|
+
async #initializeNewSessionState(
|
|
7820
|
+
nextDiscoverySessionToolNames: string[] | undefined,
|
|
7821
|
+
previousSessionFile: string | undefined,
|
|
7822
|
+
): Promise<void> {
|
|
7672
7823
|
const inheritedThinkingLevel = resolveThinkingLevelForModel(this.model, this.#getInheritedThinkingLevel());
|
|
7673
7824
|
this.#thinkingLevel = inheritedThinkingLevel;
|
|
7674
7825
|
this.agent.setThinkingLevel(toReasoningEffort(inheritedThinkingLevel));
|
|
@@ -7687,15 +7838,11 @@ export class AgentSession {
|
|
|
7687
7838
|
this.sessionFile,
|
|
7688
7839
|
this.#getConfiguredDefaultSelectedMCPToolNames(),
|
|
7689
7840
|
);
|
|
7690
|
-
|
|
7691
7841
|
this.#todoReminderCount = 0;
|
|
7692
7842
|
this.#planReferenceSent = false;
|
|
7693
7843
|
this.#planReferencePath = "local://PLAN.md";
|
|
7694
7844
|
this.#reconnectToAgent();
|
|
7695
|
-
|
|
7696
7845
|
this.#resetIrcRosterDeliveryState();
|
|
7697
|
-
|
|
7698
|
-
// Emit session_switch event with reason "new" to hooks
|
|
7699
7846
|
if (this.#extensionRunner) {
|
|
7700
7847
|
await this.#extensionRunner.emit({
|
|
7701
7848
|
type: "session_switch",
|
|
@@ -7703,8 +7850,6 @@ export class AgentSession {
|
|
|
7703
7850
|
previousSessionFile,
|
|
7704
7851
|
});
|
|
7705
7852
|
}
|
|
7706
|
-
|
|
7707
|
-
return true;
|
|
7708
7853
|
}
|
|
7709
7854
|
|
|
7710
7855
|
/**
|
|
@@ -10291,11 +10436,13 @@ export class AgentSession {
|
|
|
10291
10436
|
sessionId: string | undefined;
|
|
10292
10437
|
providerSessionState: Map<string, ProviderSessionState>;
|
|
10293
10438
|
preferWebsockets: boolean | undefined;
|
|
10439
|
+
remoteCompactionFallbackHealth: RemoteCompactionFallbackHealthHooks;
|
|
10294
10440
|
} {
|
|
10295
10441
|
return {
|
|
10296
10442
|
sessionId: this.agent.providerSessionId ?? this.agent.sessionId,
|
|
10297
10443
|
providerSessionState: this.#providerSessionState,
|
|
10298
10444
|
preferWebsockets: this.agent.preferWebsockets,
|
|
10445
|
+
remoteCompactionFallbackHealth: this.#remoteCompactionFallbackHealth,
|
|
10299
10446
|
};
|
|
10300
10447
|
}
|
|
10301
10448
|
|
package/src/task/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@gajae
|
|
|
19
19
|
import type { Model, Usage } from "@gajae-code/ai";
|
|
20
20
|
import { $env, prompt, Snowflake } from "@gajae-code/utils";
|
|
21
21
|
import type { ToolSession } from "..";
|
|
22
|
-
import { AsyncJobManager } from "../async";
|
|
22
|
+
import { AsyncJobManager, OwnerSubagentShutdownError, type ResumeRunner } from "../async";
|
|
23
23
|
import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
24
24
|
import type { Theme } from "../modes/theme/theme";
|
|
25
25
|
import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
|
|
@@ -547,8 +547,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
547
547
|
};
|
|
548
548
|
|
|
549
549
|
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
550
|
+
let resumeRunner: ResumeRunner | undefined;
|
|
550
551
|
if (typeof manager.setResumeRunner === "function") {
|
|
551
|
-
|
|
552
|
+
resumeRunner = (_subagentId, message, resumeDescriptor) => {
|
|
552
553
|
const descriptor = isTaskResumeDescriptor(resumeDescriptor?.data) ? resumeDescriptor.data : undefined;
|
|
553
554
|
if (!descriptor) return undefined;
|
|
554
555
|
const forkSeeds = descriptor.forkContextSeed
|
|
@@ -590,7 +591,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
590
591
|
},
|
|
591
592
|
},
|
|
592
593
|
);
|
|
593
|
-
}
|
|
594
|
+
};
|
|
595
|
+
manager.setResumeRunner(resumeRunner);
|
|
594
596
|
}
|
|
595
597
|
const semaphore = new Semaphore(maxConcurrency);
|
|
596
598
|
const buildForkContextSeedForTask = async (task: TaskItem): Promise<ForkContextSeed | undefined> => {
|
|
@@ -770,18 +772,21 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
770
772
|
);
|
|
771
773
|
startedJobs.push({ jobId, taskId: taskItem.id });
|
|
772
774
|
if (typeof manager.registerResumeDescriptor === "function") {
|
|
773
|
-
manager.registerResumeDescriptor(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
775
|
+
manager.registerResumeDescriptor(
|
|
776
|
+
{
|
|
777
|
+
subagentId: uniqueId,
|
|
778
|
+
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
779
|
+
data: {
|
|
780
|
+
toolCallId: _toolCallId,
|
|
781
|
+
params,
|
|
782
|
+
task: { ...taskItem, id: uniqueId },
|
|
783
|
+
sessionFile: subtaskSessionFile,
|
|
784
|
+
forkContextSeed: frozenForkSeed,
|
|
785
|
+
agentSource: fallbackAgentSource,
|
|
786
|
+
} satisfies TaskResumeDescriptor,
|
|
787
|
+
},
|
|
788
|
+
resumeRunner,
|
|
789
|
+
);
|
|
785
790
|
}
|
|
786
791
|
if (typeof manager.registerSubagentRecord === "function") {
|
|
787
792
|
manager.registerSubagentRecord({
|
|
@@ -795,7 +800,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
795
800
|
});
|
|
796
801
|
}
|
|
797
802
|
} catch (error) {
|
|
798
|
-
const message =
|
|
803
|
+
const message =
|
|
804
|
+
error instanceof OwnerSubagentShutdownError
|
|
805
|
+
? error.message
|
|
806
|
+
: error instanceof Error
|
|
807
|
+
? error.message
|
|
808
|
+
: String(error);
|
|
799
809
|
failedSchedules.push(`${taskItem.id}: ${message}`);
|
|
800
810
|
const progress = progressByTaskId.get(taskItem.id);
|
|
801
811
|
if (progress) {
|