@gajae-code/coding-agent 0.18.6 → 0.18.7
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 +39 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/config/settings.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +6 -0
- package/dist/types/sdk/router/session-router.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
- package/dist/types/session/session-retirement.d.ts +47 -0
- package/dist/types/session/session-storage.d.ts +0 -46
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -0
- package/package.json +7 -7
- package/scripts/resident-memory-bench.ts +174 -20
- package/src/capability/index.ts +1 -0
- package/src/capability/types.ts +4 -0
- package/src/cli/config-cli.ts +8 -18
- package/src/cli-main.ts +7 -8
- package/src/commands/sdk.ts +112 -21
- package/src/config/settings.ts +44 -4
- package/src/discovery/helpers.ts +9 -3
- package/src/discovery/ssh.ts +8 -3
- package/src/gjc-runtime/gc-runtime.ts +2 -1
- package/src/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/acp/acp-agent.ts +75 -3
- package/src/runtime-mcp/client.ts +13 -4
- package/src/runtime-mcp/elicitation.ts +142 -0
- package/src/sdk/broker/broker.ts +8 -1
- package/src/sdk/broker/lifecycle.ts +109 -7
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +4 -2
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/session-runtime.test.ts +43 -2
- package/src/sdk/host/session-runtime.ts +2 -2
- package/src/sdk/mcp/server.ts +15 -3
- package/src/sdk/router/session-router.ts +19 -2
- package/src/sdk/session.ts +14 -0
- package/src/session/agent-session.ts +57 -6
- package/src/session/internal/managed-session-storage.ts +107 -1
- package/src/session/internal/task-artifact-owner-access.ts +229 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
- package/src/session/session-retirement.ts +155 -0
- package/src/session/session-storage.ts +1 -160
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +578 -0
- package/src/session/task-artifact-owner-retirement.ts +475 -0
- package/src/session/task-artifact-owner.ts +289 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { NativeExactUnlinkResult } from "@gajae-code/natives";
|
|
2
|
+
import {
|
|
3
|
+
assertSessionRoot,
|
|
4
|
+
parseTaskArtifactOwnerDeletionEvidence,
|
|
5
|
+
parseTaskArtifactOwnerRetirementContinuation,
|
|
6
|
+
parseTaskArtifactOwnerRetirementOutcome,
|
|
7
|
+
type TaskArtifactOwnerDeletionEvidence,
|
|
8
|
+
type TaskArtifactOwnerRetirementContinuation,
|
|
9
|
+
type TaskArtifactOwnerRetirementOutcome,
|
|
10
|
+
type TaskArtifactOwnerStorageContext,
|
|
11
|
+
} from "../../session/task-artifact-owner-codec";
|
|
12
|
+
|
|
13
|
+
export type BrokerCleanupPhase = "artifacts" | "transcript" | "metadata" | "lifecycle";
|
|
14
|
+
|
|
15
|
+
/** Exact owner-disposition wire shape. Evidence and continuation are supplied separately by cleanup replay. */
|
|
16
|
+
export type BrokerTaskArtifactOwnerRetirementDisposition =
|
|
17
|
+
| { readonly kind: "completed"; readonly nativeOutcome?: NativeExactUnlinkResult }
|
|
18
|
+
| {
|
|
19
|
+
readonly kind: "payload_retired";
|
|
20
|
+
readonly namespace: "retained";
|
|
21
|
+
readonly nativeOutcome: NativeExactUnlinkResult;
|
|
22
|
+
}
|
|
23
|
+
| { readonly kind: "cleanup_pending"; readonly nativeOutcome?: NativeExactUnlinkResult }
|
|
24
|
+
| { readonly kind: "uncertain"; readonly reason: string; readonly nativeOutcome?: NativeExactUnlinkResult };
|
|
25
|
+
|
|
26
|
+
/** Owner-only projection of durable cleanup state accepted by the pure validator. */
|
|
27
|
+
export interface BrokerTaskArtifactOwnerCleanupFields {
|
|
28
|
+
readonly taskArtifactOwnerDeletionEvidence?: unknown;
|
|
29
|
+
readonly taskArtifactOwnerRetirementContinuation?: unknown;
|
|
30
|
+
readonly taskArtifactOwnerRetirementOutcome?: unknown;
|
|
31
|
+
readonly taskArtifactOwnerPayloadRetired?: unknown;
|
|
32
|
+
readonly taskArtifactOwnerNamespaceRetained?: unknown;
|
|
33
|
+
readonly taskArtifactOwnerRetired?: unknown;
|
|
34
|
+
readonly taskArtifactOwnerTranscriptDeleted?: unknown;
|
|
35
|
+
readonly taskArtifactOwnerCleanupError?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Fully decoded owner-only cleanup state for an authorized session and phase. */
|
|
39
|
+
export interface BrokerTaskArtifactOwnerCleanupValidation {
|
|
40
|
+
readonly deletionEvidence?: TaskArtifactOwnerDeletionEvidence;
|
|
41
|
+
readonly retirementContinuation?: TaskArtifactOwnerRetirementContinuation;
|
|
42
|
+
readonly retirementOutcome?: TaskArtifactOwnerRetirementOutcome;
|
|
43
|
+
readonly payloadRetired?: true;
|
|
44
|
+
readonly namespaceRetained?: true;
|
|
45
|
+
readonly retired?: true;
|
|
46
|
+
readonly transcriptDeleted?: true;
|
|
47
|
+
readonly cleanupDiagnostic?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
55
|
+
return Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function invalidDisposition(): never {
|
|
59
|
+
throw new Error("task_artifact_owner_retirement_disposition_invalid");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decode the owner-only wire disposition using its exact trusted evidence and continuation.
|
|
64
|
+
* A decoded `completed` value is historical DTO data, not proof of physical retirement or writer closure.
|
|
65
|
+
*/
|
|
66
|
+
export function parseBrokerTaskArtifactOwnerRetirementDisposition(
|
|
67
|
+
value: unknown,
|
|
68
|
+
context: TaskArtifactOwnerStorageContext,
|
|
69
|
+
evidence: TaskArtifactOwnerDeletionEvidence,
|
|
70
|
+
continuation: TaskArtifactOwnerRetirementContinuation | undefined,
|
|
71
|
+
): TaskArtifactOwnerRetirementOutcome {
|
|
72
|
+
assertSessionRoot(context);
|
|
73
|
+
if (!isRecord(value)) return invalidDisposition();
|
|
74
|
+
const kind = value.kind;
|
|
75
|
+
let expectedKeys: readonly string[];
|
|
76
|
+
switch (kind) {
|
|
77
|
+
case "completed":
|
|
78
|
+
case "cleanup_pending":
|
|
79
|
+
expectedKeys = Object.hasOwn(value, "nativeOutcome") ? ["kind", "nativeOutcome"] : ["kind"];
|
|
80
|
+
break;
|
|
81
|
+
case "payload_retired":
|
|
82
|
+
expectedKeys = ["kind", "namespace", "nativeOutcome"];
|
|
83
|
+
break;
|
|
84
|
+
case "uncertain":
|
|
85
|
+
expectedKeys = Object.hasOwn(value, "nativeOutcome")
|
|
86
|
+
? ["kind", "reason", "nativeOutcome"]
|
|
87
|
+
: ["kind", "reason"];
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
return invalidDisposition();
|
|
91
|
+
}
|
|
92
|
+
if (!hasExactKeys(value, expectedKeys)) return invalidDisposition();
|
|
93
|
+
const reconstructed = {
|
|
94
|
+
...value,
|
|
95
|
+
evidence,
|
|
96
|
+
...(continuation !== undefined ? { continuation } : {}),
|
|
97
|
+
};
|
|
98
|
+
return parseTaskArtifactOwnerRetirementOutcome(context, evidence, reconstructed);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function serializeNativeOutcome(outcome: NativeExactUnlinkResult): NativeExactUnlinkResult {
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
ok: outcome.ok,
|
|
104
|
+
...(outcome.code !== undefined ? { code: outcome.code } : {}),
|
|
105
|
+
...(outcome.detachedPath !== undefined ? { detachedPath: outcome.detachedPath } : {}),
|
|
106
|
+
...(outcome.retainedSuccessorPath !== undefined ? { retainedSuccessorPath: outcome.retainedSuccessorPath } : {}),
|
|
107
|
+
...(outcome.retainedPlaceholderPath !== undefined
|
|
108
|
+
? { retainedPlaceholderPath: outcome.retainedPlaceholderPath }
|
|
109
|
+
: {}),
|
|
110
|
+
...(outcome.retainedUnknownPath !== undefined ? { retainedUnknownPath: outcome.retainedUnknownPath } : {}),
|
|
111
|
+
...(outcome.payloadDurable !== undefined ? { payloadDurable: outcome.payloadDurable } : {}),
|
|
112
|
+
...(outcome.windowsErrorCode !== undefined ? { windowsErrorCode: outcome.windowsErrorCode } : {}),
|
|
113
|
+
}) as NativeExactUnlinkResult;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Project an already validated internal outcome to the exact broker wire disposition. */
|
|
117
|
+
export function serializeBrokerTaskArtifactOwnerRetirementDisposition(
|
|
118
|
+
outcome: TaskArtifactOwnerRetirementOutcome,
|
|
119
|
+
): BrokerTaskArtifactOwnerRetirementDisposition {
|
|
120
|
+
switch (outcome.kind) {
|
|
121
|
+
case "completed":
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
kind: outcome.kind,
|
|
124
|
+
...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
|
|
125
|
+
});
|
|
126
|
+
case "payload_retired":
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
kind: outcome.kind,
|
|
129
|
+
namespace: outcome.namespace,
|
|
130
|
+
nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome),
|
|
131
|
+
});
|
|
132
|
+
case "cleanup_pending":
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
kind: outcome.kind,
|
|
135
|
+
...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
|
|
136
|
+
});
|
|
137
|
+
case "uncertain":
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
kind: outcome.kind,
|
|
140
|
+
reason: outcome.reason,
|
|
141
|
+
...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
|
|
142
|
+
});
|
|
143
|
+
default: {
|
|
144
|
+
const exhaustive: never = outcome;
|
|
145
|
+
throw new Error(`task_artifact_owner_retirement_outcome_unknown:${String(exhaustive)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const OWNER_CLEANUP_KEYS = new Set([
|
|
151
|
+
"taskArtifactOwnerDeletionEvidence",
|
|
152
|
+
"taskArtifactOwnerRetirementContinuation",
|
|
153
|
+
"taskArtifactOwnerRetirementOutcome",
|
|
154
|
+
"taskArtifactOwnerPayloadRetired",
|
|
155
|
+
"taskArtifactOwnerNamespaceRetained",
|
|
156
|
+
"taskArtifactOwnerRetired",
|
|
157
|
+
"taskArtifactOwnerTranscriptDeleted",
|
|
158
|
+
"taskArtifactOwnerCleanupError",
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Strictly decode the owner-field projection of cleanup state against trusted session, scope context, phase,
|
|
163
|
+
* and artifact-removal state. Empty/legacy owner fields decode to an empty result. A decoded completion
|
|
164
|
+
* remains historical data: any later effect must separately verify current physical absence, fresh scrub
|
|
165
|
+
* proof, native success and current owner-parent/side-path authority before it mutates storage or transcripts.
|
|
166
|
+
*/
|
|
167
|
+
export function decodeBrokerTaskArtifactOwnerCleanupFields(
|
|
168
|
+
value: unknown,
|
|
169
|
+
context: TaskArtifactOwnerStorageContext,
|
|
170
|
+
expectedSessionId: string,
|
|
171
|
+
phase: BrokerCleanupPhase,
|
|
172
|
+
artifactsRemoved: boolean | undefined,
|
|
173
|
+
): BrokerTaskArtifactOwnerCleanupValidation {
|
|
174
|
+
assertSessionRoot(context);
|
|
175
|
+
if (!isRecord(value) || Object.keys(value).some(key => !OWNER_CLEANUP_KEYS.has(key)))
|
|
176
|
+
throw new Error("task_artifact_owner_cleanup_fields_invalid");
|
|
177
|
+
if (expectedSessionId.length === 0 || (artifactsRemoved !== undefined && typeof artifactsRemoved !== "boolean"))
|
|
178
|
+
throw new Error("task_artifact_owner_cleanup_fields_invalid");
|
|
179
|
+
|
|
180
|
+
const has = (key: string) => Object.hasOwn(value, key);
|
|
181
|
+
const trueFlag = (key: string): true | undefined => {
|
|
182
|
+
if (!has(key)) return undefined;
|
|
183
|
+
if (value[key] !== true) throw new Error("task_artifact_owner_cleanup_flags_invalid");
|
|
184
|
+
return true;
|
|
185
|
+
};
|
|
186
|
+
let deletionEvidence: TaskArtifactOwnerDeletionEvidence | undefined;
|
|
187
|
+
if (has("taskArtifactOwnerDeletionEvidence")) {
|
|
188
|
+
deletionEvidence = parseTaskArtifactOwnerDeletionEvidence(value.taskArtifactOwnerDeletionEvidence);
|
|
189
|
+
if (deletionEvidence.sessionId !== expectedSessionId)
|
|
190
|
+
throw new Error("task_artifact_owner_cleanup_session_mismatch");
|
|
191
|
+
}
|
|
192
|
+
let retirementContinuation: TaskArtifactOwnerRetirementContinuation | undefined;
|
|
193
|
+
if (has("taskArtifactOwnerRetirementContinuation")) {
|
|
194
|
+
if (!deletionEvidence) throw new Error("task_artifact_owner_cleanup_evidence_missing");
|
|
195
|
+
retirementContinuation = parseTaskArtifactOwnerRetirementContinuation(
|
|
196
|
+
context,
|
|
197
|
+
deletionEvidence,
|
|
198
|
+
value.taskArtifactOwnerRetirementContinuation,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
let retirementOutcome: TaskArtifactOwnerRetirementOutcome | undefined;
|
|
202
|
+
if (has("taskArtifactOwnerRetirementOutcome")) {
|
|
203
|
+
if (!deletionEvidence) throw new Error("task_artifact_owner_cleanup_evidence_missing");
|
|
204
|
+
retirementOutcome = parseBrokerTaskArtifactOwnerRetirementDisposition(
|
|
205
|
+
value.taskArtifactOwnerRetirementOutcome,
|
|
206
|
+
context,
|
|
207
|
+
deletionEvidence,
|
|
208
|
+
retirementContinuation,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const payloadRetired = trueFlag("taskArtifactOwnerPayloadRetired");
|
|
213
|
+
const namespaceRetained = trueFlag("taskArtifactOwnerNamespaceRetained");
|
|
214
|
+
const retired = trueFlag("taskArtifactOwnerRetired");
|
|
215
|
+
const transcriptDeleted = trueFlag("taskArtifactOwnerTranscriptDeleted");
|
|
216
|
+
let cleanupDiagnostic: string | undefined;
|
|
217
|
+
if (has("taskArtifactOwnerCleanupError")) {
|
|
218
|
+
const diagnostic = value.taskArtifactOwnerCleanupError;
|
|
219
|
+
if (typeof diagnostic !== "string" || diagnostic.length === 0 || diagnostic.length > 4096)
|
|
220
|
+
throw new Error("task_artifact_owner_cleanup_diagnostic_invalid");
|
|
221
|
+
cleanupDiagnostic = diagnostic;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (
|
|
225
|
+
(retired !== undefined && (payloadRetired !== undefined || namespaceRetained !== undefined)) ||
|
|
226
|
+
(retired !== undefined && retirementOutcome?.kind !== "completed") ||
|
|
227
|
+
(payloadRetired !== undefined) !== (namespaceRetained !== undefined) ||
|
|
228
|
+
((retired !== undefined ||
|
|
229
|
+
payloadRetired !== undefined ||
|
|
230
|
+
namespaceRetained !== undefined ||
|
|
231
|
+
transcriptDeleted !== undefined) &&
|
|
232
|
+
(!deletionEvidence || !retirementOutcome)) ||
|
|
233
|
+
(transcriptDeleted !== undefined &&
|
|
234
|
+
(phase !== "artifacts" || artifactsRemoved !== true || retirementOutcome === undefined))
|
|
235
|
+
)
|
|
236
|
+
throw new Error("task_artifact_owner_cleanup_state_invalid");
|
|
237
|
+
|
|
238
|
+
if (retirementOutcome) {
|
|
239
|
+
if (
|
|
240
|
+
(retired !== undefined && retirementOutcome.kind !== "completed") ||
|
|
241
|
+
(retirementOutcome.kind === "completed" && retired === undefined) ||
|
|
242
|
+
(payloadRetired !== undefined &&
|
|
243
|
+
(retirementOutcome.kind !== "payload_retired" || retirementContinuation === undefined)) ||
|
|
244
|
+
(retirementOutcome.kind === "payload_retired" && payloadRetired === undefined)
|
|
245
|
+
)
|
|
246
|
+
throw new Error("task_artifact_owner_cleanup_disposition_mismatch");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
deletionEvidence,
|
|
251
|
+
retirementContinuation,
|
|
252
|
+
retirementOutcome,
|
|
253
|
+
payloadRetired,
|
|
254
|
+
namespaceRetained,
|
|
255
|
+
retired,
|
|
256
|
+
transcriptDeleted,
|
|
257
|
+
cleanupDiagnostic,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -141,8 +141,8 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
141
141
|
* crashes, recovery, and attachment retirement cannot replay ambiguous work.
|
|
142
142
|
*/
|
|
143
143
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
144
|
-
discord:
|
|
145
|
-
slack:
|
|
144
|
+
discord: 83,
|
|
145
|
+
slack: 90,
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -318,6 +318,9 @@ export class ChatDaemonRuntime {
|
|
|
318
318
|
this.#router = new SessionRouter({
|
|
319
319
|
agentDir: input.agentDir,
|
|
320
320
|
observer: true,
|
|
321
|
+
// A lean final answer carries no messageRef, so the raw native copy of a
|
|
322
|
+
// positioned frame has no identity to collapse onto and would post twice.
|
|
323
|
+
positionedNotificationEffects: true,
|
|
321
324
|
correlateFrame,
|
|
322
325
|
deps: {
|
|
323
326
|
...deps.routerDeps,
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
canDeliverSdkEvent,
|
|
106
106
|
createSdkSurfaceFactory,
|
|
107
107
|
masterAttestationForEffectiveHost,
|
|
108
|
+
POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY,
|
|
108
109
|
reattestMasterSessionIdentity,
|
|
109
110
|
SESSION_HOST_OBSERVER_CAPABILITY,
|
|
110
111
|
type SessionSdkHost,
|
|
@@ -204,7 +205,6 @@ import {
|
|
|
204
205
|
ASK_SELECTED_ACK_CAPABILITY,
|
|
205
206
|
type EnsureDaemonResult,
|
|
206
207
|
ensureTelegramDaemonRunningDetailed,
|
|
207
|
-
POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY,
|
|
208
208
|
} from "./telegram-daemon";
|
|
209
209
|
|
|
210
210
|
export type {
|
|
@@ -3722,8 +3722,8 @@ function sdkControlSurface(
|
|
|
3722
3722
|
return unavailable("model.cycle", "no session model-cycle seam is installed")();
|
|
3723
3723
|
return { changed: (await ctx.cycleModel()) !== undefined };
|
|
3724
3724
|
},
|
|
3725
|
-
setThinking: level => {
|
|
3726
|
-
api.
|
|
3725
|
+
setThinking: async level => {
|
|
3726
|
+
await api.setThinkingLevelForControl(level as ThinkingLevel, false);
|
|
3727
3727
|
return { changed: true };
|
|
3728
3728
|
},
|
|
3729
3729
|
cycleThinking: () => {
|
|
@@ -71,6 +71,7 @@ export {
|
|
|
71
71
|
TELEGRAM_TRANSPORT_GENERATION,
|
|
72
72
|
} from "./telegram-daemon-contract";
|
|
73
73
|
|
|
74
|
+
import { POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY } from "../host/host";
|
|
74
75
|
import {
|
|
75
76
|
type AgentDirSessionLifecycleService,
|
|
76
77
|
createSessionLifecycleService,
|
|
@@ -303,8 +304,9 @@ export const ASK_CONTROLS_CAPABILITY = "ask_controls_v1";
|
|
|
303
304
|
export const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v2";
|
|
304
305
|
/** Receive-only compatibility capability for pre-v2 hosts. */
|
|
305
306
|
export const LEGACY_TOOL_ACTIVITY_CAPABILITY = "tool_activity_v1";
|
|
306
|
-
/**
|
|
307
|
-
export
|
|
307
|
+
/** Public `sdk/bus/telegram-daemon` path for the canonical `sdk/host` capability token. */
|
|
308
|
+
export { POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY };
|
|
309
|
+
|
|
308
310
|
type ToolActivityCapability = "v1" | "v2";
|
|
309
311
|
|
|
310
312
|
function negotiateToolActivityCapability(
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -107,6 +107,12 @@ export const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v2";
|
|
|
107
107
|
export const TURN_STREAM_CAPABILITY = "turn_stream";
|
|
108
108
|
/** Capability used by notification adapters that observe a host without owning its work. */
|
|
109
109
|
export const SESSION_HOST_OBSERVER_CAPABILITY = "session_host_observer_v1";
|
|
110
|
+
/**
|
|
111
|
+
* Opts notification adapters into positioned-only delivery for matching live
|
|
112
|
+
* effects: the host excludes a negotiating connection from the raw native leg
|
|
113
|
+
* of every frame its positioned envelope already reached.
|
|
114
|
+
*/
|
|
115
|
+
export const POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY = "positioned_notification_effects_v1";
|
|
110
116
|
export const CAP_GATED_FRAME_KINDS: ReadonlySet<string> = new Set(["tool_activity", "reasoning_summary"]);
|
|
111
117
|
const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
|
|
112
118
|
|
|
@@ -3,8 +3,9 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import { markNonDispatchedToolEvent } from "@gajae-code/agent-core";
|
|
6
|
+
import { markNonDispatchedToolEvent, ThinkingLevel } from "@gajae-code/agent-core";
|
|
7
7
|
import { logger } from "@gajae-code/utils";
|
|
8
|
+
import { createTestSession } from "../../../test/utilities";
|
|
8
9
|
import { AsyncJobManager } from "../../async";
|
|
9
10
|
import type { Settings } from "../../config/settings";
|
|
10
11
|
import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
|
|
@@ -103,7 +104,46 @@ test("runtime capabilities preserve explicit primary control surface", () => {
|
|
|
103
104
|
expect(createSdkCapabilities(policy, false, "cli")).toMatchObject({ primaryControlSurface: "cli" });
|
|
104
105
|
});
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
test("thinking.set uses the control setter and reports the applied session level", async () => {
|
|
108
|
+
const sessionContext = await createTestSession({ inMemory: true });
|
|
109
|
+
const { session, tempDir: cwd, cleanup } = sessionContext;
|
|
110
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
111
|
+
const api = {
|
|
112
|
+
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
113
|
+
handlers.set(event, handler);
|
|
114
|
+
},
|
|
115
|
+
sendUserMessage: async () => {},
|
|
116
|
+
setThinkingLevel: () => {
|
|
117
|
+
throw new Error("thinking.set must not use the legacy setter");
|
|
118
|
+
},
|
|
119
|
+
setThinkingLevelForControl: async (level: ThinkingLevel, persist: boolean) => {
|
|
120
|
+
expect(persist).toBe(false);
|
|
121
|
+
await session.setThinkingLevelForControl(level, persist);
|
|
122
|
+
},
|
|
123
|
+
} as unknown as ExtensionAPI;
|
|
124
|
+
const response = Promise.withResolvers<SdkFrame>();
|
|
125
|
+
const transport = memoryTransport(frame => {
|
|
126
|
+
if (frame.type === "control_response" && frame.id === "thinking-set") response.resolve(frame);
|
|
127
|
+
});
|
|
128
|
+
createSdkSessionRuntimeExtension(api, { agentDir: cwd, createTransport: async () => transport });
|
|
129
|
+
const ctx = extensionContext(transport.sessionId, cwd);
|
|
130
|
+
try {
|
|
131
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
132
|
+
transport.feed("client", {
|
|
133
|
+
type: "control_request",
|
|
134
|
+
id: "thinking-set",
|
|
135
|
+
operation: "thinking.set",
|
|
136
|
+
input: { level: ThinkingLevel.Medium },
|
|
137
|
+
} as SdkFrame);
|
|
138
|
+
await response.promise;
|
|
139
|
+
expect(session.thinkingLevel).toBe(ThinkingLevel.Medium);
|
|
140
|
+
} finally {
|
|
141
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
142
|
+
await cleanup();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
function memoryTransport(onSend?: (frame: SdkFrame) => void): SessionSdkTransport & {
|
|
107
147
|
feed(connectionId: string, frame: SdkFrame): void;
|
|
108
148
|
readonly sent: SdkFrame[];
|
|
109
149
|
readonly broadcasts: SdkFrame[];
|
|
@@ -126,6 +166,7 @@ function memoryTransport(): SessionSdkTransport & {
|
|
|
126
166
|
},
|
|
127
167
|
sendFrame(_connectionId, frame) {
|
|
128
168
|
sent.push(frame);
|
|
169
|
+
onSend?.(frame);
|
|
129
170
|
},
|
|
130
171
|
start: async () => {
|
|
131
172
|
started = true;
|
|
@@ -4372,8 +4372,8 @@ function createControlSurface(
|
|
|
4372
4372
|
},
|
|
4373
4373
|
setModelProfile: id => (ctx.setModelProfile ? ctx.setModelProfile(id) : unavailable("model.profile.set")()),
|
|
4374
4374
|
cycleModel: () => (ctx.cycleModel ? ctx.cycleModel() : unavailable("model.cycle")()),
|
|
4375
|
-
setThinking: level => {
|
|
4376
|
-
api.
|
|
4375
|
+
setThinking: async level => {
|
|
4376
|
+
await api.setThinkingLevelForControl(level as ThinkingLevel, false);
|
|
4377
4377
|
return { changed: true };
|
|
4378
4378
|
},
|
|
4379
4379
|
cycleThinking: () =>
|
package/src/sdk/mcp/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAgentDir } from "@gajae-code/utils";
|
|
1
|
+
import { getAgentDir, VERSION } from "@gajae-code/utils";
|
|
2
2
|
import { ensureBroker } from "../broker/ensure";
|
|
3
3
|
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
4
4
|
import { SdkClientError } from "../client/client";
|
|
@@ -14,6 +14,8 @@ import { type SessionAttachment, SessionRouter, SessionRouterError } from "../ro
|
|
|
14
14
|
import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../session-list";
|
|
15
15
|
|
|
16
16
|
const PROTOCOL_VERSION = "2024-11-05";
|
|
17
|
+
const SUPPORTED_PROTOCOL_VERSIONS = [PROTOCOL_VERSION, "2025-03-26", "2025-06-18"] as const;
|
|
18
|
+
const NEWEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1];
|
|
17
19
|
const SERVER_NAME = "gjc-sdk-mcp";
|
|
18
20
|
const ENDPOINT_CREDENTIAL_OPERATION = "session.get_endpoint";
|
|
19
21
|
|
|
@@ -107,6 +109,16 @@ function isObject(value: unknown): value is Arguments {
|
|
|
107
109
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
function negotiatedProtocolVersion(params: unknown): string {
|
|
113
|
+
const requestedProtocolVersion = isObject(params) ? params.protocolVersion : undefined;
|
|
114
|
+
if (
|
|
115
|
+
typeof requestedProtocolVersion === "string" &&
|
|
116
|
+
(SUPPORTED_PROTOCOL_VERSIONS as readonly string[]).includes(requestedProtocolVersion)
|
|
117
|
+
)
|
|
118
|
+
return requestedProtocolVersion;
|
|
119
|
+
return NEWEST_PROTOCOL_VERSION;
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
function asString(args: Arguments, name: string): string | null {
|
|
111
123
|
return typeof args[name] === "string" && args[name] ? (args[name] as string) : null;
|
|
112
124
|
}
|
|
@@ -389,9 +401,9 @@ export function createSdkMcpServer(options: SdkMcpServerOptions = {}) {
|
|
|
389
401
|
jsonrpc: "2.0",
|
|
390
402
|
id,
|
|
391
403
|
result: {
|
|
392
|
-
protocolVersion:
|
|
404
|
+
protocolVersion: negotiatedProtocolVersion(request.params),
|
|
393
405
|
capabilities: { tools: {} },
|
|
394
|
-
serverInfo: { name: SERVER_NAME },
|
|
406
|
+
serverInfo: { name: SERVER_NAME, version: VERSION },
|
|
395
407
|
},
|
|
396
408
|
};
|
|
397
409
|
if (request.method === "tools/list")
|
|
@@ -25,7 +25,11 @@ import {
|
|
|
25
25
|
readSdkSessionEndpoint,
|
|
26
26
|
type SdkSessionEndpoint,
|
|
27
27
|
} from "../client/discovery";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY,
|
|
30
|
+
SESSION_HOST_OBSERVER_CAPABILITY,
|
|
31
|
+
TURN_STREAM_CAPABILITY,
|
|
32
|
+
} from "../host/host";
|
|
29
33
|
import {
|
|
30
34
|
type ActivatedPreparedSession,
|
|
31
35
|
type PreparedSessionActivationClient,
|
|
@@ -291,6 +295,13 @@ export interface SessionRouterOptions {
|
|
|
291
295
|
attachFilter?: (sessionId: string) => boolean;
|
|
292
296
|
/** Marks default SDK clients as notification-only observers; demanding by default. */
|
|
293
297
|
observer?: boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Negotiates positioned-only notification effects on default SDK clients, so the
|
|
300
|
+
* host stops sending them the raw native copy of frames whose positioned
|
|
301
|
+
* envelope already reached them. Only notification-effect publishers opt in;
|
|
302
|
+
* ordinary SDK observers keep both public surfaces.
|
|
303
|
+
*/
|
|
304
|
+
positionedNotificationEffects?: boolean;
|
|
294
305
|
deps?: SessionRouterDeps;
|
|
295
306
|
/** Runtime-specific identity validation; Router supplies a conservative fallback. */
|
|
296
307
|
correlateFrame?: SessionRouterFrameCorrelator;
|
|
@@ -619,6 +630,7 @@ export class SessionRouter {
|
|
|
619
630
|
readonly #sessionIds: ReadonlySet<string> | undefined;
|
|
620
631
|
readonly #attachFilter: ((sessionId: string) => boolean) | undefined;
|
|
621
632
|
readonly #observer: boolean;
|
|
633
|
+
readonly #positionedNotificationEffects: boolean;
|
|
622
634
|
readonly #deps: SessionRouterDeps;
|
|
623
635
|
readonly #correlateFrame: SessionRouterFrameCorrelator;
|
|
624
636
|
readonly #index: SessionIndex;
|
|
@@ -662,6 +674,7 @@ export class SessionRouter {
|
|
|
662
674
|
this.#sessionIds = options.sessionIds === undefined ? undefined : new Set(options.sessionIds);
|
|
663
675
|
this.#attachFilter = options.attachFilter;
|
|
664
676
|
this.#observer = options.observer === true;
|
|
677
|
+
this.#positionedNotificationEffects = options.positionedNotificationEffects === true;
|
|
665
678
|
this.#deps = options.deps ?? {};
|
|
666
679
|
this.#correlateFrame = options.correlateFrame ?? fallbackCorrelation;
|
|
667
680
|
this.#index = this.#deps.createIndex?.(options.agentDir) ?? new DefaultSessionIndex(options.agentDir);
|
|
@@ -1530,9 +1543,13 @@ export class SessionRouter {
|
|
|
1530
1543
|
endpointMtimeMs,
|
|
1531
1544
|
});
|
|
1532
1545
|
} else {
|
|
1546
|
+
const capabilities = [
|
|
1547
|
+
...(this.#observer ? [SESSION_HOST_OBSERVER_CAPABILITY, TURN_STREAM_CAPABILITY] : []),
|
|
1548
|
+
...(this.#positionedNotificationEffects ? [POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY] : []),
|
|
1549
|
+
];
|
|
1533
1550
|
const defaultClient = new SdkClient(endpoint.url, endpoint.token, {
|
|
1534
1551
|
...ACP_SESSION_RECONNECT,
|
|
1535
|
-
...(
|
|
1552
|
+
...(capabilities.length > 0 ? { capabilities } : {}),
|
|
1536
1553
|
});
|
|
1537
1554
|
transport = defaultClient;
|
|
1538
1555
|
connection = defaultClient.connect().then(() => defaultClient);
|
package/src/sdk/session.ts
CHANGED
|
@@ -161,6 +161,7 @@ import {
|
|
|
161
161
|
type MCPToolCache,
|
|
162
162
|
resolveMCPToolCache,
|
|
163
163
|
} from "../runtime-mcp";
|
|
164
|
+
import { createMCPFormInputHandler } from "../runtime-mcp/elicitation";
|
|
164
165
|
import type { MCPLoadResult } from "../runtime-mcp/manager";
|
|
165
166
|
import { MCP_STARTUP_WAIT_GRACE_MS } from "../runtime-mcp/startup-policy";
|
|
166
167
|
import type { MCPServerConfig } from "../runtime-mcp/types";
|
|
@@ -2653,7 +2654,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2653
2654
|
let ownedMcpManagerTools: readonly CustomTool[] = [];
|
|
2654
2655
|
let publishOwnedMcpTools = false;
|
|
2655
2656
|
const notificationDebounceTimers = new Map<string, Timer>();
|
|
2657
|
+
const installMcpInputHandler = (manager: MCPManager): void => {
|
|
2658
|
+
manager.setInputRequestHandler(
|
|
2659
|
+
createMCPFormInputHandler({
|
|
2660
|
+
getUi: () => {
|
|
2661
|
+
const context = toolContextStore.getContext();
|
|
2662
|
+
return { ui: context.ui, hasUI: context.hasUI === true };
|
|
2663
|
+
},
|
|
2664
|
+
getAskAnswerSource: () => session.getAskAnswerSource(),
|
|
2665
|
+
}),
|
|
2666
|
+
);
|
|
2667
|
+
};
|
|
2656
2668
|
const wireMcpManagerCallbacks = (manager: MCPManager): void => {
|
|
2669
|
+
installMcpInputHandler(manager);
|
|
2657
2670
|
manager.setOnPromptsChanged(serverName => {
|
|
2658
2671
|
const promptCommands = buildMCPPromptCommands(manager);
|
|
2659
2672
|
session.setMCPPromptCommands(promptCommands);
|
|
@@ -3685,6 +3698,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3685
3698
|
: {}),
|
|
3686
3699
|
});
|
|
3687
3700
|
owned.setAuthStorage(authStorage);
|
|
3701
|
+
installMcpInputHandler(owned);
|
|
3688
3702
|
mcpManager = owned;
|
|
3689
3703
|
ownsMcpManager = true;
|
|
3690
3704
|
registerOwnedMcpManagerCleanup(owned);
|