@jsonstudio/llms 0.6.954 → 0.6.1164

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.
Files changed (130) hide show
  1. package/dist/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
  2. package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
  3. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
  4. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
  5. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
  11. package/dist/conversion/hub/ops/operations.d.ts +19 -0
  12. package/dist/conversion/hub/ops/operations.js +126 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
  14. package/dist/conversion/hub/pipeline/hub-pipeline.js +489 -19
  15. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
  16. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  17. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  18. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  19. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  20. package/dist/conversion/hub/process/chat-process.js +252 -41
  21. package/dist/conversion/hub/response/provider-response.js +175 -2
  22. package/dist/conversion/hub/response/response-runtime.js +1 -1
  23. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  25. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
  27. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
  29. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  31. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  33. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  34. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  35. package/dist/conversion/shared/bridge-policies.js +5 -105
  36. package/dist/conversion/shared/gemini-tool-utils.js +89 -15
  37. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  38. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  39. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  40. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  41. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  42. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  43. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  44. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  45. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  46. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  47. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  48. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  49. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  50. package/dist/router/virtual-router/bootstrap.js +54 -5
  51. package/dist/router/virtual-router/engine-selection.js +132 -42
  52. package/dist/router/virtual-router/engine.d.ts +3 -0
  53. package/dist/router/virtual-router/engine.js +142 -33
  54. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  55. package/dist/router/virtual-router/health-weighted.js +63 -0
  56. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  57. package/dist/router/virtual-router/load-balancer.js +45 -16
  58. package/dist/router/virtual-router/routing-instructions.js +17 -1
  59. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  60. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  61. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  62. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  63. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  64. package/dist/router/virtual-router/types.d.ts +70 -0
  65. package/dist/servertool/clock/config.d.ts +7 -0
  66. package/dist/servertool/clock/config.js +27 -0
  67. package/dist/servertool/clock/daemon.d.ts +3 -0
  68. package/dist/servertool/clock/daemon.js +79 -0
  69. package/dist/servertool/clock/io.d.ts +2 -0
  70. package/dist/servertool/clock/io.js +13 -0
  71. package/dist/servertool/clock/paths.d.ts +4 -0
  72. package/dist/servertool/clock/paths.js +25 -0
  73. package/dist/servertool/clock/session-store.d.ts +3 -0
  74. package/dist/servertool/clock/session-store.js +56 -0
  75. package/dist/servertool/clock/state.d.ts +5 -0
  76. package/dist/servertool/clock/state.js +62 -0
  77. package/dist/servertool/clock/task-store.d.ts +5 -0
  78. package/dist/servertool/clock/task-store.js +4 -0
  79. package/dist/servertool/clock/tasks.d.ts +17 -0
  80. package/dist/servertool/clock/tasks.js +221 -0
  81. package/dist/servertool/clock/types.d.ts +36 -0
  82. package/dist/servertool/clock/types.js +1 -0
  83. package/dist/servertool/engine.d.ts +2 -0
  84. package/dist/servertool/engine.js +161 -7
  85. package/dist/servertool/followup-shadow.d.ts +16 -0
  86. package/dist/servertool/followup-shadow.js +145 -0
  87. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  88. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  89. package/dist/servertool/handlers/clock-auto.js +160 -0
  90. package/dist/servertool/handlers/clock.d.ts +1 -0
  91. package/dist/servertool/handlers/clock.js +197 -0
  92. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  93. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  94. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  95. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  96. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  97. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  98. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  99. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  100. package/dist/servertool/handlers/vision.d.ts +7 -1
  101. package/dist/servertool/handlers/vision.js +61 -117
  102. package/dist/servertool/handlers/web-search.d.ts +7 -1
  103. package/dist/servertool/handlers/web-search.js +122 -105
  104. package/dist/servertool/reenter-backend.d.ts +23 -0
  105. package/dist/servertool/reenter-backend.js +18 -0
  106. package/dist/servertool/server-side-tools.d.ts +3 -2
  107. package/dist/servertool/server-side-tools.js +64 -10
  108. package/dist/servertool/types.d.ts +92 -3
  109. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  110. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  111. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  112. package/dist/sse/shared/writer.js +24 -7
  113. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  114. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  115. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  116. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  117. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  118. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  119. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  120. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  121. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  122. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  123. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  124. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  125. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  126. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  127. package/dist/tools/apply-patch/validation/shared.js +6 -0
  128. package/dist/tools/apply-patch/validator.d.ts +2 -2
  129. package/dist/tools/apply-patch/validator.js +6 -556
  130. package/package.json +1 -1
@@ -0,0 +1,160 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
3
+ import { findNextUndeliveredDueAtMs, listClockTasks, normalizeClockConfig, startClockDaemonIfNeeded } from '../clock/task-store.js';
4
+ const FLOW_ID = 'clock_hold_flow';
5
+ function resolveClientConnectionState(value) {
6
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
7
+ return null;
8
+ }
9
+ const record = value;
10
+ if (typeof record.disconnected !== 'boolean') {
11
+ return null;
12
+ }
13
+ return { disconnected: record.disconnected };
14
+ }
15
+ function isStopFinishReason(base) {
16
+ if (!base || typeof base !== 'object' || Array.isArray(base)) {
17
+ return false;
18
+ }
19
+ const payload = base;
20
+ const choicesRaw = payload.choices;
21
+ if (Array.isArray(choicesRaw) && choicesRaw.length) {
22
+ const first = choicesRaw[0];
23
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
24
+ return false;
25
+ }
26
+ const finishReasonRaw = first.finish_reason;
27
+ const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
28
+ ? finishReasonRaw.trim().toLowerCase()
29
+ : '';
30
+ if (!finishReason || finishReason === 'tool_calls') {
31
+ return false;
32
+ }
33
+ if (finishReason !== 'stop' && finishReason !== 'length') {
34
+ return false;
35
+ }
36
+ const message = first.message &&
37
+ typeof first.message === 'object' &&
38
+ !Array.isArray(first.message)
39
+ ? first.message
40
+ : null;
41
+ if (!message) {
42
+ return false;
43
+ }
44
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
45
+ if (toolCalls.length > 0) {
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+ const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
51
+ if (statusRaw && statusRaw !== 'completed') {
52
+ return false;
53
+ }
54
+ if (payload.required_action && typeof payload.required_action === 'object') {
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ function resolveSessionId(adapterContext) {
60
+ if (!adapterContext || typeof adapterContext !== 'object' || Array.isArray(adapterContext)) {
61
+ return null;
62
+ }
63
+ const sessionId = typeof adapterContext.sessionId === 'string' ? String(adapterContext.sessionId).trim() : '';
64
+ return sessionId || null;
65
+ }
66
+ function computeHoldSleepMs(remainingMs) {
67
+ if (remainingMs <= 0)
68
+ return 0;
69
+ if (remainingMs > 10 * 60_000)
70
+ return 30_000;
71
+ if (remainingMs > 60_000)
72
+ return 10_000;
73
+ if (remainingMs > 10_000)
74
+ return 1_000;
75
+ return 200;
76
+ }
77
+ async function sleep(ms) {
78
+ if (!Number.isFinite(ms) || ms <= 0) {
79
+ return;
80
+ }
81
+ await new Promise((resolve) => setTimeout(resolve, ms));
82
+ }
83
+ const handler = async (ctx) => {
84
+ const record = ctx.adapterContext;
85
+ // Only trigger on stop/length completion (no tool calls).
86
+ if (!isStopFinishReason(ctx.base)) {
87
+ return null;
88
+ }
89
+ // When client already disconnected, skip holding.
90
+ const connectionState = resolveClientConnectionState(record.clientConnectionState);
91
+ if (connectionState?.disconnected === true) {
92
+ return null;
93
+ }
94
+ const clientDisconnectedRaw = record.clientDisconnected;
95
+ if (clientDisconnectedRaw === true ||
96
+ (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
97
+ return null;
98
+ }
99
+ const clockConfig = normalizeClockConfig(record.clock);
100
+ if (!clockConfig) {
101
+ return null;
102
+ }
103
+ await startClockDaemonIfNeeded(clockConfig);
104
+ const sessionId = resolveSessionId(ctx.adapterContext);
105
+ if (!sessionId) {
106
+ return null;
107
+ }
108
+ const seed = extractCapturedChatSeed(record.capturedChatRequest);
109
+ if (!seed) {
110
+ return null;
111
+ }
112
+ const tasks = await listClockTasks(sessionId, clockConfig);
113
+ const at = Date.now();
114
+ const nextDueAtMs = findNextUndeliveredDueAtMs(tasks, at);
115
+ if (!nextDueAtMs) {
116
+ return null;
117
+ }
118
+ // Wait until the "due window" is reached (now >= dueAt - dueWindowMs).
119
+ const thresholdMs = nextDueAtMs - clockConfig.dueWindowMs;
120
+ while (Date.now() < thresholdMs) {
121
+ const state = resolveClientConnectionState(ctx.adapterContext.clientConnectionState);
122
+ if (state?.disconnected === true) {
123
+ return null;
124
+ }
125
+ const remaining = thresholdMs - Date.now();
126
+ await sleep(computeHoldSleepMs(remaining));
127
+ // Best-effort: if tasks were cleared/cancelled while holding, stop holding.
128
+ try {
129
+ const refreshed = await listClockTasks(sessionId, clockConfig);
130
+ const refreshedNext = findNextUndeliveredDueAtMs(refreshed, Date.now());
131
+ if (!refreshedNext) {
132
+ return null;
133
+ }
134
+ }
135
+ catch {
136
+ // ignore refresh errors; keep holding
137
+ }
138
+ }
139
+ return {
140
+ flowId: FLOW_ID,
141
+ finalize: async () => ({
142
+ chatResponse: ctx.base,
143
+ execution: {
144
+ flowId: FLOW_ID,
145
+ followup: {
146
+ requestIdSuffix: ':clock_hold_followup',
147
+ entryEndpoint: ctx.entryEndpoint,
148
+ injection: {
149
+ ops: [
150
+ { op: 'append_assistant_message', required: false },
151
+ { op: 'append_user_text', text: 'continue' }
152
+ ]
153
+ },
154
+ metadata: (connectionState ? { clientConnectionState: connectionState } : {})
155
+ }
156
+ }
157
+ })
158
+ };
159
+ };
160
+ registerServerToolHandler('clock_auto', handler, { trigger: 'auto' });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,197 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { cloneJson } from '../server-side-tools.js';
3
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
4
+ import { cancelClockTask, clearClockTasks, listClockTasks, normalizeClockConfig, parseDueAtMs, scheduleClockTasks, startClockDaemonIfNeeded } from '../clock/task-store.js';
5
+ const FLOW_ID = 'clock_flow';
6
+ function parseToolArguments(toolCall) {
7
+ if (!toolCall.arguments || typeof toolCall.arguments !== 'string') {
8
+ return {};
9
+ }
10
+ try {
11
+ const parsed = JSON.parse(toolCall.arguments);
12
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ function resolveSessionId(adapterContext) {
19
+ if (!adapterContext || typeof adapterContext !== 'object' || Array.isArray(adapterContext)) {
20
+ return null;
21
+ }
22
+ const sessionId = typeof adapterContext.sessionId === 'string' ? String(adapterContext.sessionId).trim() : '';
23
+ return sessionId || null;
24
+ }
25
+ function injectClockToolOutput(base, toolCall, content) {
26
+ const cloned = cloneJson(base);
27
+ const existingOutputs = Array.isArray(cloned.tool_outputs)
28
+ ? cloned.tool_outputs
29
+ : [];
30
+ let payloadText;
31
+ if (typeof content === 'string') {
32
+ payloadText = content;
33
+ }
34
+ else {
35
+ try {
36
+ payloadText = JSON.stringify(content ?? {});
37
+ }
38
+ catch {
39
+ payloadText = String(content ?? '');
40
+ }
41
+ }
42
+ cloned.tool_outputs = [
43
+ ...existingOutputs,
44
+ {
45
+ tool_call_id: toolCall.id,
46
+ name: 'clock',
47
+ content: payloadText
48
+ }
49
+ ];
50
+ return cloned;
51
+ }
52
+ function asPlainObject(value) {
53
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
54
+ return value;
55
+ }
56
+ return null;
57
+ }
58
+ function normalizeAction(value) {
59
+ const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
60
+ if (raw === 'list' || raw === 'cancel' || raw === 'clear' || raw === 'schedule') {
61
+ return raw;
62
+ }
63
+ return 'schedule';
64
+ }
65
+ function toIso(ms) {
66
+ try {
67
+ return new Date(ms).toISOString();
68
+ }
69
+ catch {
70
+ return new Date(0).toISOString();
71
+ }
72
+ }
73
+ function normalizeScheduleItems(parsed) {
74
+ const itemsRaw = parsed.items;
75
+ if (!Array.isArray(itemsRaw) || itemsRaw.length === 0) {
76
+ return { ok: false, items: [], message: 'clock.schedule requires items: [{ dueAt, task, tool?, arguments? }]' };
77
+ }
78
+ const items = [];
79
+ for (const entry of itemsRaw) {
80
+ const rec = asPlainObject(entry);
81
+ if (!rec) {
82
+ return { ok: false, items: [], message: 'clock.schedule items must be objects' };
83
+ }
84
+ const dueAtMs = parseDueAtMs(rec.dueAt);
85
+ if (!dueAtMs) {
86
+ return { ok: false, items: [], message: 'clock.schedule dueAt must be an ISO8601 string' };
87
+ }
88
+ const task = typeof rec.task === 'string' ? rec.task.trim() : '';
89
+ if (!task) {
90
+ return { ok: false, items: [], message: 'clock.schedule task must be a non-empty string' };
91
+ }
92
+ const tool = typeof rec.tool === 'string' && rec.tool.trim().length ? rec.tool.trim() : undefined;
93
+ const argsObj = rec.arguments && typeof rec.arguments === 'object' && !Array.isArray(rec.arguments)
94
+ ? cloneJson(rec.arguments)
95
+ : undefined;
96
+ items.push({
97
+ dueAtMs,
98
+ task,
99
+ ...(tool ? { tool } : {}),
100
+ ...(argsObj ? { arguments: argsObj } : {})
101
+ });
102
+ }
103
+ return { ok: true, items };
104
+ }
105
+ function mapTaskForTool(t) {
106
+ return {
107
+ taskId: t.taskId,
108
+ dueAt: toIso(t.dueAtMs),
109
+ task: t.task,
110
+ ...(t.tool ? { tool: t.tool } : {}),
111
+ ...(t.arguments ? { arguments: t.arguments } : {}),
112
+ ...(typeof t.deliveredAtMs === 'number' ? { deliveredAt: toIso(t.deliveredAtMs) } : {}),
113
+ deliveryCount: t.deliveryCount
114
+ };
115
+ }
116
+ const handler = async (ctx) => {
117
+ const toolCall = ctx.toolCall;
118
+ if (!toolCall || toolCall.name !== 'clock') {
119
+ return null;
120
+ }
121
+ const clockConfig = normalizeClockConfig(ctx.adapterContext.clock);
122
+ const sessionId = resolveSessionId(ctx.adapterContext);
123
+ const parsedArgs = parseToolArguments(toolCall);
124
+ const action = normalizeAction(parsedArgs.action);
125
+ const respond = (payload) => {
126
+ return {
127
+ flowId: FLOW_ID,
128
+ finalize: async () => {
129
+ const patched = injectClockToolOutput(ctx.base, toolCall, payload);
130
+ const seed = extractCapturedChatSeed(ctx.adapterContext?.capturedChatRequest);
131
+ const canFollowup = Boolean(seed);
132
+ return {
133
+ chatResponse: patched,
134
+ execution: {
135
+ flowId: FLOW_ID,
136
+ ...(canFollowup
137
+ ? {
138
+ followup: {
139
+ requestIdSuffix: ':clock_followup',
140
+ entryEndpoint: ctx.entryEndpoint,
141
+ injection: {
142
+ ops: [
143
+ { op: 'append_assistant_message', required: true },
144
+ { op: 'append_tool_messages_from_tool_outputs', required: true }
145
+ ]
146
+ }
147
+ }
148
+ }
149
+ : {})
150
+ }
151
+ };
152
+ }
153
+ };
154
+ };
155
+ if (!clockConfig) {
156
+ return respond({
157
+ ok: false,
158
+ action,
159
+ message: 'clock tool is not enabled (virtualrouter.clock.enabled=true required).'
160
+ });
161
+ }
162
+ await startClockDaemonIfNeeded(clockConfig);
163
+ if (!sessionId) {
164
+ return respond({
165
+ ok: false,
166
+ action,
167
+ message: 'clock requires sessionId (x-session-id header or metadata.sessionId).'
168
+ });
169
+ }
170
+ if (action === 'list') {
171
+ const items = await listClockTasks(sessionId, clockConfig);
172
+ return respond({ ok: true, action, items: items.map(mapTaskForTool) });
173
+ }
174
+ if (action === 'clear') {
175
+ const removedCount = await clearClockTasks(sessionId, clockConfig);
176
+ return respond({ ok: true, action, removedCount });
177
+ }
178
+ if (action === 'cancel') {
179
+ const taskId = typeof parsedArgs.taskId === 'string' ? parsedArgs.taskId.trim() : '';
180
+ if (!taskId) {
181
+ return respond({ ok: false, action, message: 'clock.cancel requires taskId' });
182
+ }
183
+ const removed = await cancelClockTask(sessionId, taskId, clockConfig);
184
+ return respond({ ok: true, action, removed: removed ? taskId : null });
185
+ }
186
+ const normalized = normalizeScheduleItems(parsedArgs);
187
+ if (!normalized.ok) {
188
+ return respond({ ok: false, action, message: normalized.message ?? 'invalid schedule items' });
189
+ }
190
+ const scheduled = await scheduleClockTasks(sessionId, normalized.items, clockConfig);
191
+ return respond({
192
+ ok: true,
193
+ action,
194
+ scheduled: scheduled.map(mapTaskForTool)
195
+ });
196
+ };
197
+ registerServerToolHandler('clock', handler);