@gajae-code/coding-agent 0.12.8 → 0.12.10
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 +29 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +23 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -0
- package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/package.json +8 -8
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +12 -6
- package/src/commands/sdk.ts +54 -0
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +4 -1
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/controllers/selector-controller.ts +61 -43
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/notification-service.ts +140 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/session.ts +24 -7
- package/src/session/agent-session.ts +65 -13
- package/src/session/internal/managed-session-storage.ts +36 -2
- package/src/session/internal/native-publish-outcome.ts +12 -2
- package/src/slash-commands/builtin-registry.ts +35 -11
- package/src/system-prompt.ts +28 -15
- package/src/tools/bash.ts +3 -1
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
|
@@ -9,6 +9,10 @@ import { theme } from "../../../modes/theme/theme";
|
|
|
9
9
|
import { shortenPath } from "../../../tools/render-utils";
|
|
10
10
|
import type { Extension, ExtensionState } from "./types";
|
|
11
11
|
|
|
12
|
+
function renderSeparator(width: number): string {
|
|
13
|
+
return theme.fg("dim", theme.boxSharp.horizontal.repeat(Math.max(0, Math.min(width - 2, 40))));
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export class InspectorPanel implements Component {
|
|
13
17
|
#extension: Extension | null = null;
|
|
14
18
|
|
|
@@ -106,7 +110,7 @@ export class InspectorPanel implements Component {
|
|
|
106
110
|
#renderFilePreview(raw: unknown, width: number): string[] {
|
|
107
111
|
const lines: string[] = [];
|
|
108
112
|
lines.push(theme.fg("muted", "Preview:"));
|
|
109
|
-
lines.push(
|
|
113
|
+
lines.push(renderSeparator(width));
|
|
110
114
|
|
|
111
115
|
const content = this.#getContextFileContent(raw);
|
|
112
116
|
if (!content) {
|
|
@@ -164,7 +168,7 @@ export class InspectorPanel implements Component {
|
|
|
164
168
|
#renderToolArgs(raw: unknown, width: number): string[] {
|
|
165
169
|
const lines: string[] = [];
|
|
166
170
|
lines.push(theme.fg("muted", "Arguments:"));
|
|
167
|
-
lines.push(
|
|
171
|
+
lines.push(renderSeparator(width));
|
|
168
172
|
|
|
169
173
|
try {
|
|
170
174
|
const tool = raw as any;
|
|
@@ -203,7 +207,7 @@ export class InspectorPanel implements Component {
|
|
|
203
207
|
#renderSkillContent(raw: unknown, width: number): string[] {
|
|
204
208
|
const lines: string[] = [];
|
|
205
209
|
lines.push(theme.fg("muted", "Instruction:"));
|
|
206
|
-
lines.push(
|
|
210
|
+
lines.push(renderSeparator(width));
|
|
207
211
|
|
|
208
212
|
try {
|
|
209
213
|
const skill = raw as any;
|
|
@@ -232,7 +236,7 @@ export class InspectorPanel implements Component {
|
|
|
232
236
|
#renderMcpDetails(raw: unknown, width: number): string[] {
|
|
233
237
|
const lines: string[] = [];
|
|
234
238
|
lines.push(theme.fg("muted", "Connection:"));
|
|
235
|
-
lines.push(
|
|
239
|
+
lines.push(renderSeparator(width));
|
|
236
240
|
|
|
237
241
|
try {
|
|
238
242
|
const mcp = raw as any;
|
|
@@ -271,7 +275,7 @@ export class InspectorPanel implements Component {
|
|
|
271
275
|
// Show trigger pattern if present
|
|
272
276
|
if (ext.trigger) {
|
|
273
277
|
lines.push(theme.fg("muted", "Trigger:"));
|
|
274
|
-
lines.push(
|
|
278
|
+
lines.push(renderSeparator(width));
|
|
275
279
|
lines.push(` ${theme.fg("accent", ext.trigger)}`);
|
|
276
280
|
lines.push("");
|
|
277
281
|
}
|
|
@@ -201,6 +201,11 @@ const CALLBACK_SERVER_PROVIDERS = new Set<string>([
|
|
|
201
201
|
|
|
202
202
|
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
|
|
203
203
|
|
|
204
|
+
/** Providers whose authorization server can display a code for `/login <provider> --manual`. */
|
|
205
|
+
const MANUAL_CODE_PROVIDERS = new Set<string>(["anthropic"]);
|
|
206
|
+
|
|
207
|
+
const MANUAL_CODE_LOGIN_TIP = "Tip: You can complete pairing with /login <code>.";
|
|
208
|
+
|
|
204
209
|
function isThemePreviewSuperseded(result: { success: boolean; error?: string }): boolean {
|
|
205
210
|
return !result.success && result.error?.includes("superseded by a newer request") === true;
|
|
206
211
|
}
|
|
@@ -2945,7 +2950,14 @@ export class SelectorController {
|
|
|
2945
2950
|
});
|
|
2946
2951
|
}
|
|
2947
2952
|
|
|
2948
|
-
async #handleOAuthLogin(providerId: string): Promise<void> {
|
|
2953
|
+
async #handleOAuthLogin(providerId: string, options?: OAuthSelectorOptions): Promise<void> {
|
|
2954
|
+
const manualCode = options?.manualCode === true;
|
|
2955
|
+
if (manualCode && !MANUAL_CODE_PROVIDERS.has(providerId)) {
|
|
2956
|
+
this.ctx.showError(
|
|
2957
|
+
`${providerId} has no paste-a-code login. Supported: ${[...MANUAL_CODE_PROVIDERS].join(", ")}.`,
|
|
2958
|
+
);
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2949
2961
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
2950
2962
|
const manualInput = this.ctx.oauthManualInput;
|
|
2951
2963
|
const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
|
|
@@ -2953,51 +2965,57 @@ export class SelectorController {
|
|
|
2953
2965
|
this.ctx.showStatus("Checking the local OpenCodex proxy…");
|
|
2954
2966
|
}
|
|
2955
2967
|
try {
|
|
2956
|
-
await this.ctx.session.modelRegistry.authStorage.login(
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
2961
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
2962
|
-
if (info.instructions) {
|
|
2968
|
+
await this.ctx.session.modelRegistry.authStorage.login(
|
|
2969
|
+
providerId as OAuthProvider,
|
|
2970
|
+
{
|
|
2971
|
+
onAuth: (info: { url: string; instructions?: string }) => {
|
|
2963
2972
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
2964
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("
|
|
2965
|
-
|
|
2966
|
-
|
|
2973
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
2974
|
+
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
2975
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
2976
|
+
if (info.instructions) {
|
|
2977
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
2978
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
2979
|
+
}
|
|
2980
|
+
if (useManualInput) {
|
|
2981
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
2982
|
+
this.ctx.chatContainer.addChild(
|
|
2983
|
+
new Text(theme.fg("dim", manualCode ? MANUAL_CODE_LOGIN_TIP : MANUAL_LOGIN_TIP), 1, 0),
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
this.ctx.ui.requestRender();
|
|
2987
|
+
this.ctx.openInBrowser(info.url);
|
|
2988
|
+
},
|
|
2989
|
+
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
2967
2990
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
2968
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
codeInput.onSubmit = () => {
|
|
2983
|
-
const code = codeInput.getValue();
|
|
2991
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
|
|
2992
|
+
if (prompt.placeholder) {
|
|
2993
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
|
|
2994
|
+
}
|
|
2995
|
+
this.ctx.ui.requestRender();
|
|
2996
|
+
const { promise, resolve } = Promise.withResolvers<string>();
|
|
2997
|
+
const codeInput = new Input();
|
|
2998
|
+
codeInput.onSubmit = () => {
|
|
2999
|
+
const code = codeInput.getValue();
|
|
3000
|
+
this.ctx.editorContainer.clear();
|
|
3001
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
3002
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
3003
|
+
resolve(code);
|
|
3004
|
+
};
|
|
2984
3005
|
this.ctx.editorContainer.clear();
|
|
2985
|
-
this.ctx.editorContainer.addChild(
|
|
2986
|
-
this.ctx.ui.setFocus(
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
onProgress: (message: string) => {
|
|
2996
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
|
2997
|
-
this.ctx.ui.requestRender();
|
|
3006
|
+
this.ctx.editorContainer.addChild(codeInput);
|
|
3007
|
+
this.ctx.ui.setFocus(codeInput);
|
|
3008
|
+
this.ctx.ui.requestRender();
|
|
3009
|
+
return promise;
|
|
3010
|
+
},
|
|
3011
|
+
onProgress: (message: string) => {
|
|
3012
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
|
3013
|
+
this.ctx.ui.requestRender();
|
|
3014
|
+
},
|
|
3015
|
+
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
|
|
2998
3016
|
},
|
|
2999
|
-
|
|
3000
|
-
|
|
3017
|
+
{ manualCode },
|
|
3018
|
+
);
|
|
3001
3019
|
await this.ctx.session.modelRegistry.refresh();
|
|
3002
3020
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
3003
3021
|
const successMessage =
|
|
@@ -3050,7 +3068,7 @@ export class SelectorController {
|
|
|
3050
3068
|
return;
|
|
3051
3069
|
}
|
|
3052
3070
|
if (mode === "login") {
|
|
3053
|
-
await this.#handleOAuthLogin(providerId);
|
|
3071
|
+
await this.#handleOAuthLogin(providerId, options);
|
|
3054
3072
|
} else {
|
|
3055
3073
|
await this.#handleOAuthLogout(providerId);
|
|
3056
3074
|
}
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -198,6 +198,9 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
198
198
|
let unsubscribe: (() => void) | undefined;
|
|
199
199
|
|
|
200
200
|
try {
|
|
201
|
+
for (const warning of new Set(session.configWarnings)) {
|
|
202
|
+
await writeStderrAndQuiesce(`Warning: ${warning}\n`);
|
|
203
|
+
}
|
|
201
204
|
// Emit session header for JSON mode.
|
|
202
205
|
if (mode === "json") {
|
|
203
206
|
const header = session.sessionManager.getHeader();
|
package/src/modes/types.ts
CHANGED
|
@@ -495,4 +495,10 @@ export interface OAuthSelectorOptions {
|
|
|
495
495
|
allowExternalCredentialDiscovery?: boolean;
|
|
496
496
|
trigger?: "bare-login";
|
|
497
497
|
externalCredentialDiscover?: CredentialAutoImportOptions["discover"];
|
|
498
|
+
/**
|
|
499
|
+
* Pair by pasting the code the provider displays instead of waiting on the
|
|
500
|
+
* loopback callback. Set by `/login <provider> --manual` for browsers that
|
|
501
|
+
* cannot reach this machine.
|
|
502
|
+
*/
|
|
503
|
+
manualCode?: boolean;
|
|
498
504
|
}
|
|
@@ -97,6 +97,7 @@ Do not claim a web search, browser action, integration, or subagent ran without
|
|
|
97
97
|
<inputs>
|
|
98
98
|
- Keep tool inputs concise where possible.
|
|
99
99
|
- For `path` or path-like fields, prefer relative paths.
|
|
100
|
+
- Write non-ASCII text in tool inputs as literal UTF-8, NEVER as hand-spelled `\uXXXX` escapes, including JSON serialized into a string field (no `ensure_ascii`-style output there). Escapes that are the intended source syntax of the file you are writing — character-class ranges, codepoint bounds — are unaffected.
|
|
100
101
|
{{#if intentTracing}}
|
|
101
102
|
- Most tools have a `{{intentField}}` parameter. Fill it with a concise intent in present participle form, 2-6 words, no period, capitalized.
|
|
102
103
|
{{/if}}
|
|
@@ -16,6 +16,7 @@ export interface DaemonPaths {
|
|
|
16
16
|
ownership: string;
|
|
17
17
|
roots: string;
|
|
18
18
|
steal: string;
|
|
19
|
+
diagnostic: string;
|
|
19
20
|
aliases: string;
|
|
20
21
|
seenUpdates: string;
|
|
21
22
|
}
|
|
@@ -31,6 +32,7 @@ export function daemonPaths(agentDir: string): DaemonPaths {
|
|
|
31
32
|
roots: path.join(dir, "telegram-daemon.roots.json"),
|
|
32
33
|
steal: path.join(dir, "telegram-daemon.steal"),
|
|
33
34
|
aliases: path.join(dir, "telegram-callback-aliases.json"),
|
|
35
|
+
diagnostic: path.join(dir, "telegram-daemon.diagnostics.json"),
|
|
34
36
|
seenUpdates: path.join(dir, "telegram-seen-updates.json"),
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -63,6 +63,49 @@ export function sanitizeDiagnostic(text: string, token?: string): string {
|
|
|
63
63
|
return out.replace(TELEGRAM_TOKEN_PATTERN, "<redacted>");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export interface NotificationDiagnosticEvent {
|
|
67
|
+
timestamp: string;
|
|
68
|
+
operation: string;
|
|
69
|
+
phase: string;
|
|
70
|
+
outcome: string;
|
|
71
|
+
reason: string;
|
|
72
|
+
pid?: number;
|
|
73
|
+
incarnation?: string;
|
|
74
|
+
ageMs?: number;
|
|
75
|
+
detail?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Append a bounded, private, secret-safe daemon diagnostic event. */
|
|
79
|
+
export async function writeNotificationDiagnostic(
|
|
80
|
+
settings: Pick<Settings, "getAgentDir">,
|
|
81
|
+
event: Omit<NotificationDiagnosticEvent, "timestamp"> & { timestamp?: string },
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const file = daemonPaths(settings.getAgentDir()).diagnostic;
|
|
84
|
+
const dir = path.dirname(file);
|
|
85
|
+
let events: NotificationDiagnosticEvent[] = [];
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(await fsPromises.readFile(file, "utf8")) as { events?: unknown };
|
|
88
|
+
if (Array.isArray(parsed.events)) events = parsed.events as NotificationDiagnosticEvent[];
|
|
89
|
+
} catch {
|
|
90
|
+
// A missing or corrupt diagnostic history must not block notification recovery.
|
|
91
|
+
}
|
|
92
|
+
const safe: NotificationDiagnosticEvent = {
|
|
93
|
+
timestamp: event.timestamp ?? new Date().toISOString(),
|
|
94
|
+
operation: event.operation.slice(0, 80),
|
|
95
|
+
phase: event.phase.slice(0, 80),
|
|
96
|
+
outcome: event.outcome.slice(0, 80),
|
|
97
|
+
reason: event.reason.slice(0, 120),
|
|
98
|
+
...(event.pid === undefined ? {} : { pid: event.pid }),
|
|
99
|
+
...(event.incarnation === undefined ? {} : { incarnation: event.incarnation.slice(0, 120) }),
|
|
100
|
+
...(event.ageMs === undefined ? {} : { ageMs: Math.max(0, Math.floor(event.ageMs)) }),
|
|
101
|
+
...(event.detail === undefined ? {} : { detail: sanitizeDiagnostic(event.detail).slice(0, 512) }),
|
|
102
|
+
};
|
|
103
|
+
await fsPromises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
104
|
+
await fsPromises.writeFile(file, `${JSON.stringify({ version: 1, events: [...events, safe].slice(-64) })}\n`, {
|
|
105
|
+
mode: 0o600,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
66
109
|
function sanitizeProviderDiagnostic(text: string, cfg: NotificationConfig, provider: NotificationProvider): string {
|
|
67
110
|
const secrets =
|
|
68
111
|
provider === "telegram"
|
|
@@ -182,6 +225,7 @@ export interface NotificationServiceDeps {
|
|
|
182
225
|
fs?: NotificationServiceFs;
|
|
183
226
|
now?: () => number;
|
|
184
227
|
pidAlive?: (pid: number) => boolean;
|
|
228
|
+
pidIncarnation?: (pid: number) => string | undefined;
|
|
185
229
|
fetchImpl?: typeof fetch;
|
|
186
230
|
apiBase?: string;
|
|
187
231
|
createDiscordDiagnostic?: (config: { applicationId: string; botToken: string }) => DiscordDiagnosticProvider;
|
|
@@ -1126,6 +1170,9 @@ export interface NotificationRecoveryReport {
|
|
|
1126
1170
|
action: DaemonRecoveryAction;
|
|
1127
1171
|
detail: string;
|
|
1128
1172
|
ownerId: string | undefined;
|
|
1173
|
+
blockingReason?: string;
|
|
1174
|
+
markerAgeMs?: number;
|
|
1175
|
+
forceCommand?: string;
|
|
1129
1176
|
pid: number | undefined;
|
|
1130
1177
|
};
|
|
1131
1178
|
}
|
|
@@ -1134,6 +1181,7 @@ export interface RecoveryOptions {
|
|
|
1134
1181
|
settings: Settings;
|
|
1135
1182
|
stateRoot?: string;
|
|
1136
1183
|
deps?: NotificationServiceDeps;
|
|
1184
|
+
forceDaemonLock?: boolean;
|
|
1137
1185
|
}
|
|
1138
1186
|
|
|
1139
1187
|
export interface DaemonTransitionLock {
|
|
@@ -1167,6 +1215,7 @@ type TransitionMarkerFs = {
|
|
|
1167
1215
|
writeFile?(file: string, data: string, opts?: WriteFileOptions): Promise<void>;
|
|
1168
1216
|
readEndpointFile?(file: string): Promise<NotificationEndpointFile>;
|
|
1169
1217
|
exactUnlink?(file: string, identity: NotificationEndpointFileIdentity): Promise<NotificationExactUnlinkResult>;
|
|
1218
|
+
stat?(file: string): Promise<{ mtimeMs: number }>;
|
|
1170
1219
|
};
|
|
1171
1220
|
|
|
1172
1221
|
async function readTransitionMarker(
|
|
@@ -1318,6 +1367,36 @@ function defaultTransitionPidIncarnation(pid: number): string | undefined {
|
|
|
1318
1367
|
return processIncarnation(pid);
|
|
1319
1368
|
}
|
|
1320
1369
|
|
|
1370
|
+
const TRANSITION_MARKER_GRACE_MS = 3 * HEARTBEAT_TTL_MS;
|
|
1371
|
+
|
|
1372
|
+
async function detachStaleTransitionMarker(input: {
|
|
1373
|
+
fs: TransitionMarkerFs;
|
|
1374
|
+
path: string;
|
|
1375
|
+
now: number;
|
|
1376
|
+
pidAlive: (pid: number) => boolean;
|
|
1377
|
+
pidIncarnation: (pid: number) => string | undefined;
|
|
1378
|
+
}): Promise<boolean> {
|
|
1379
|
+
const snapshot = await readTransitionMarker(input.fs, input.path);
|
|
1380
|
+
if (!snapshot) return false;
|
|
1381
|
+
let marker: unknown;
|
|
1382
|
+
try {
|
|
1383
|
+
marker = JSON.parse(snapshot.raw);
|
|
1384
|
+
} catch {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
if (!isDaemonTransitionLock(marker)) return false;
|
|
1388
|
+
const currentIncarnation = input.pidIncarnation(marker.pid);
|
|
1389
|
+
if (
|
|
1390
|
+
input.pidAlive(marker.pid) ||
|
|
1391
|
+
!isProcessIncarnation(marker.incarnation) ||
|
|
1392
|
+
(isProcessIncarnation(currentIncarnation) && currentIncarnation === marker.incarnation)
|
|
1393
|
+
)
|
|
1394
|
+
return false;
|
|
1395
|
+
const ageMs = input.now - marker.createdAt;
|
|
1396
|
+
if (!Number.isFinite(ageMs) || ageMs <= TRANSITION_MARKER_GRACE_MS) return false;
|
|
1397
|
+
return await detachTransitionMarker(input.fs, input.path, snapshot);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1321
1400
|
/**
|
|
1322
1401
|
* Owner-bound removal of a dead daemon's lock. Closes the classic
|
|
1323
1402
|
* check-then-unlink TOCTOU: a naive `unlink(lock)` after observing a dead owner
|
|
@@ -1332,20 +1411,36 @@ async function removeDeadOwnerLock(
|
|
|
1332
1411
|
paths: DaemonPaths,
|
|
1333
1412
|
pidAlive: (pid: number) => boolean,
|
|
1334
1413
|
expected: NormalizedDaemonState,
|
|
1414
|
+
now: () => number,
|
|
1415
|
+
pidIncarnation: (pid: number) => string | undefined,
|
|
1335
1416
|
): Promise<"cleared" | "contended" | "superseded" | "now-alive" | "unlink-failed"> {
|
|
1336
|
-
|
|
1417
|
+
let transition = await acquireDaemonTransitionLock({
|
|
1337
1418
|
fs,
|
|
1338
1419
|
path: paths.steal,
|
|
1339
1420
|
pid: process.pid,
|
|
1340
1421
|
pidAlive: defaultTransitionPidAlive,
|
|
1341
1422
|
pidIncarnation: defaultTransitionPidIncarnation,
|
|
1342
1423
|
});
|
|
1424
|
+
if (!transition) {
|
|
1425
|
+
await detachStaleTransitionMarker({
|
|
1426
|
+
fs,
|
|
1427
|
+
path: paths.steal,
|
|
1428
|
+
now: now(),
|
|
1429
|
+
pidAlive,
|
|
1430
|
+
pidIncarnation,
|
|
1431
|
+
});
|
|
1432
|
+
transition = await acquireDaemonTransitionLock({
|
|
1433
|
+
fs,
|
|
1434
|
+
path: paths.steal,
|
|
1435
|
+
pid: process.pid,
|
|
1436
|
+
pidAlive: defaultTransitionPidAlive,
|
|
1437
|
+
pidIncarnation: defaultTransitionPidIncarnation,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1343
1440
|
if (!transition) return "contended";
|
|
1344
1441
|
try {
|
|
1345
1442
|
const current = await readDaemonStateFile(fs, paths.state);
|
|
1346
|
-
if (!current || current.ownerId !== expected.ownerId || current.pid !== expected.pid)
|
|
1347
|
-
return "superseded";
|
|
1348
|
-
}
|
|
1443
|
+
if (!current || current.ownerId !== expected.ownerId || current.pid !== expected.pid) return "superseded";
|
|
1349
1444
|
if (pidAlive(current.pid)) return "now-alive";
|
|
1350
1445
|
if (!(await daemonTransitionLockIsHeld({ fs, path: paths.steal, lock: transition }))) return "contended";
|
|
1351
1446
|
try {
|
|
@@ -1373,6 +1468,7 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1373
1468
|
const deps = opts.deps ?? {};
|
|
1374
1469
|
const fs = deps.fs ?? nodeServiceFs;
|
|
1375
1470
|
const pidAlive = deps.pidAlive ?? defaultPidAlive;
|
|
1471
|
+
const now = deps.now ?? Date.now;
|
|
1376
1472
|
const stateRoot = opts.stateRoot ?? defaultStateRoot();
|
|
1377
1473
|
|
|
1378
1474
|
const dir = endpointDir(stateRoot);
|
|
@@ -1472,7 +1568,14 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1472
1568
|
pid: state.pid,
|
|
1473
1569
|
};
|
|
1474
1570
|
} else if (hasLock) {
|
|
1475
|
-
const outcome = await removeDeadOwnerLock(
|
|
1571
|
+
const outcome = await removeDeadOwnerLock(
|
|
1572
|
+
fs,
|
|
1573
|
+
paths,
|
|
1574
|
+
pidAlive,
|
|
1575
|
+
state,
|
|
1576
|
+
now,
|
|
1577
|
+
deps.pidIncarnation ?? defaultTransitionPidIncarnation,
|
|
1578
|
+
);
|
|
1476
1579
|
const action: DaemonRecoveryAction =
|
|
1477
1580
|
outcome === "cleared"
|
|
1478
1581
|
? "cleared-dead-owner-lock"
|
|
@@ -1491,9 +1594,35 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1491
1594
|
: outcome === "superseded"
|
|
1492
1595
|
? "a new daemon owner took over during recovery; lock left untouched"
|
|
1493
1596
|
: outcome === "contended"
|
|
1494
|
-
?
|
|
1597
|
+
? `recovery blocked by transition marker; lock left untouched${opts.forceDaemonLock ? " even after forced stale-marker retry" : ""}`
|
|
1495
1598
|
: `could not remove lock of dead owner pid ${state.pid}`;
|
|
1496
|
-
|
|
1599
|
+
const blockingReason = outcome === "contended" ? "transition-marker-unavailable-or-contended" : undefined;
|
|
1600
|
+
const markerAgeMs =
|
|
1601
|
+
blockingReason && fs.stat
|
|
1602
|
+
? (await fs.stat(paths.steal).catch(() => undefined))?.mtimeMs
|
|
1603
|
+
? now() - (await fs.stat(paths.steal).catch(() => undefined))!.mtimeMs
|
|
1604
|
+
: undefined
|
|
1605
|
+
: undefined;
|
|
1606
|
+
if (blockingReason) {
|
|
1607
|
+
await writeNotificationDiagnostic(opts.settings, {
|
|
1608
|
+
operation: "notify.recovery",
|
|
1609
|
+
phase: "recovery",
|
|
1610
|
+
outcome: action,
|
|
1611
|
+
reason: blockingReason,
|
|
1612
|
+
pid: state.pid,
|
|
1613
|
+
ageMs: markerAgeMs,
|
|
1614
|
+
detail,
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
daemon = {
|
|
1618
|
+
action,
|
|
1619
|
+
detail,
|
|
1620
|
+
ownerId: state.ownerId,
|
|
1621
|
+
pid: state.pid,
|
|
1622
|
+
...(blockingReason
|
|
1623
|
+
? { blockingReason, markerAgeMs, forceCommand: "gjc notify recovery --force-daemon-lock" }
|
|
1624
|
+
: {}),
|
|
1625
|
+
};
|
|
1497
1626
|
} else {
|
|
1498
1627
|
daemon = {
|
|
1499
1628
|
action: "none",
|
|
@@ -1533,5 +1662,9 @@ export function formatNotificationRecoveryReport(report: NotificationRecoveryRep
|
|
|
1533
1662
|
for (const unknown of report.endpointsRetainedUnknown ?? [])
|
|
1534
1663
|
lines.push(` - retained unverified cleanup path ${unknown}`);
|
|
1535
1664
|
lines.push(` daemon: ${report.daemon.action} — ${report.daemon.detail}`);
|
|
1665
|
+
if (report.daemon.blockingReason) lines.push(` - blocking reason: ${report.daemon.blockingReason}`);
|
|
1666
|
+
if (report.daemon.markerAgeMs !== undefined)
|
|
1667
|
+
lines.push(` - transition marker age: ${report.daemon.markerAgeMs}ms`);
|
|
1668
|
+
if (report.daemon.forceCommand) lines.push(` - safe escape: ${report.daemon.forceCommand}`);
|
|
1536
1669
|
return lines.join("\n");
|
|
1537
1670
|
}
|
|
@@ -55,10 +55,10 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
|
55
55
|
* ownership, settlement, and descendant cleanup authority. Generation 38 also
|
|
56
56
|
* adds durable provider-intent admission without changing owner, reclaim,
|
|
57
57
|
* signal, or spawn authority.
|
|
58
|
-
* Generation
|
|
58
|
+
* Generation 42 applies first-class provider-settings admission to Telegram
|
|
59
59
|
* lifecycle controls.
|
|
60
60
|
*/
|
|
61
|
-
export const DAEMON_GENERATION =
|
|
61
|
+
export const DAEMON_GENERATION = 42;
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
|
package/src/sdk/session.ts
CHANGED
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
buildSystemPrompt as buildSystemPromptInternal,
|
|
137
137
|
buildSystemPromptToolMetadata,
|
|
138
138
|
loadProjectContextFiles as loadContextFilesInternal,
|
|
139
|
+
loadProjectContextFilesResult as loadContextFilesResultInternal,
|
|
139
140
|
} from "../system-prompt";
|
|
140
141
|
import { AgentOutputManager } from "../task/output-manager";
|
|
141
142
|
import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
@@ -1166,10 +1167,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1166
1167
|
// Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
|
|
1167
1168
|
// at their respective consumer sites. Their work can overlap with model resolution, secret loading,
|
|
1168
1169
|
// session-context build, tool creation, MCP discovery, and extension discovery.
|
|
1169
|
-
const
|
|
1170
|
-
? Promise.resolve(options.contextFiles)
|
|
1171
|
-
: logger.time("discoverContextFiles",
|
|
1172
|
-
|
|
1170
|
+
const contextFilesResultPromise = options.contextFiles
|
|
1171
|
+
? Promise.resolve({ contextFiles: options.contextFiles, warnings: [] })
|
|
1172
|
+
: logger.time("discoverContextFiles", loadContextFilesResultInternal, { cwd });
|
|
1173
|
+
contextFilesResultPromise.catch(() => {});
|
|
1173
1174
|
const promptTemplatesPromise = options.promptTemplates
|
|
1174
1175
|
? Promise.resolve(options.promptTemplates)
|
|
1175
1176
|
: logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir);
|
|
@@ -1465,10 +1466,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1465
1466
|
}
|
|
1466
1467
|
return result;
|
|
1467
1468
|
};
|
|
1468
|
-
const [
|
|
1469
|
-
|
|
1469
|
+
const [contextFilesResult, resolvedWorkspaceTree] = await Promise.all([
|
|
1470
|
+
contextFilesResultPromise,
|
|
1470
1471
|
raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
|
|
1471
1472
|
]);
|
|
1473
|
+
const contextFiles = contextFilesResult.contextFiles;
|
|
1474
|
+
const discoveredContextFileWarnings = contextFilesResult.warnings;
|
|
1472
1475
|
|
|
1473
1476
|
const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
|
|
1474
1477
|
const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
|
|
@@ -2233,6 +2236,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2233
2236
|
settings.get("tools.intentTracing"),
|
|
2234
2237
|
options.hasUI ?? false,
|
|
2235
2238
|
);
|
|
2239
|
+
const emittedContextFileWarnings = new Set(discoveredContextFileWarnings);
|
|
2240
|
+
const contextFileWarnings = [...discoveredContextFileWarnings];
|
|
2241
|
+
for (const warning of discoveredContextFileWarnings) {
|
|
2242
|
+
logger.warn("Context file discovery warning", { warning });
|
|
2243
|
+
}
|
|
2236
2244
|
const intentField = intentTracingEnabled ? INTENT_FIELD : undefined;
|
|
2237
2245
|
const rebuildSystemPrompt = async (
|
|
2238
2246
|
toolNames: string[],
|
|
@@ -2311,14 +2319,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2311
2319
|
subagent: options.parentTaskPrefix !== undefined,
|
|
2312
2320
|
});
|
|
2313
2321
|
|
|
2322
|
+
for (const warning of defaultPrompt.warnings) {
|
|
2323
|
+
if (emittedContextFileWarnings.has(warning)) continue;
|
|
2324
|
+
emittedContextFileWarnings.add(warning);
|
|
2325
|
+
contextFileWarnings.push(warning);
|
|
2326
|
+
if (hasSession) session.configWarnings.push(warning);
|
|
2327
|
+
logger.warn("Context file discovery warning", { warning });
|
|
2328
|
+
}
|
|
2314
2329
|
if (options.systemPrompt === undefined) {
|
|
2315
2330
|
return defaultPrompt;
|
|
2316
2331
|
}
|
|
2317
2332
|
if (Array.isArray(options.systemPrompt)) {
|
|
2318
|
-
return { systemPrompt: options.systemPrompt };
|
|
2333
|
+
return { systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings };
|
|
2319
2334
|
}
|
|
2320
2335
|
return {
|
|
2321
2336
|
systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
|
|
2337
|
+
warnings: defaultPrompt.warnings,
|
|
2322
2338
|
};
|
|
2323
2339
|
};
|
|
2324
2340
|
|
|
@@ -2797,6 +2813,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2797
2813
|
forkContextSeed: options.forkContextSeed,
|
|
2798
2814
|
providerSessionState: options.providerSessionState,
|
|
2799
2815
|
});
|
|
2816
|
+
session.configWarnings.push(...contextFileWarnings);
|
|
2800
2817
|
hasSession = true;
|
|
2801
2818
|
if (asyncJobManager) {
|
|
2802
2819
|
session.yieldQueue.register<AsyncResultEntry>("async-result", {
|