clementine-agent 1.18.174 → 1.18.176
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/assistant.d.ts +2 -0
- package/dist/agent/assistant.js +12 -8
- package/dist/agent/background-tasks.d.ts +20 -8
- package/dist/agent/background-tasks.js +49 -12
- package/dist/agent/hooks.js +6 -5
- package/dist/agent/insight-engine.js +2 -2
- package/dist/agent/run-agent-heartbeat.js +4 -4
- package/dist/agent/run-agent.d.ts +4 -0
- package/dist/agent/run-agent.js +41 -4
- package/dist/agent/run-skill.js +10 -9
- package/dist/agent/team-bus.js +2 -2
- package/dist/agent/workflow-variables.js +4 -3
- package/dist/cli/cron.js +2 -2
- package/dist/cli/dashboard.js +24 -6
- package/dist/cli/routes/digest.js +4 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +31 -0
- package/dist/gateway/active-context.js +1 -1
- package/dist/gateway/context-events.d.ts +1 -1
- package/dist/gateway/cron-scheduler.d.ts +1 -0
- package/dist/gateway/cron-scheduler.js +63 -23
- package/dist/gateway/heartbeat-scheduler.js +5 -5
- package/dist/gateway/router.d.ts +6 -0
- package/dist/gateway/router.js +109 -48
- package/dist/index.js +8 -8
- package/dist/tools/admin-tools.js +11 -6
- package/dist/tools/background-task-tools.js +2 -2
- package/dist/tools/external-tools.js +3 -3
- package/dist/tools/schedule-tools.js +6 -2
- package/dist/tools/shared.js +5 -5
- package/dist/types.d.ts +10 -2
- package/package.json +1 -1
|
@@ -260,6 +260,8 @@ export declare class PersonalAssistant {
|
|
|
260
260
|
*/
|
|
261
261
|
injectContext(sessionKey: string, userText: string, assistantText: string, opts?: {
|
|
262
262
|
pending?: boolean;
|
|
263
|
+
model?: string;
|
|
264
|
+
countExchange?: boolean;
|
|
263
265
|
}): void;
|
|
264
266
|
getRecentActivity(sinceIso: string, maxEntries?: number): Array<{
|
|
265
267
|
sessionKey: string;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, BUDGET, TASK_BUDGET_TOKENS,
|
|
16
|
+
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, BUDGET, TASK_BUDGET_TOKENS, currentTimeZone, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, normalizeClaudeSdkOptionsForOneMillionContext, looksLikeClaudeOneMillionContextError, envSnapshot, } from '../config.js';
|
|
17
17
|
import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
|
|
18
18
|
import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, KNOWN_SERVICES, } from '../integrations/tool-preferences.js';
|
|
19
19
|
import { loadClaudeIntegrations } from './mcp-bridge.js';
|
|
@@ -563,17 +563,17 @@ function extractText(blocks) {
|
|
|
563
563
|
}
|
|
564
564
|
// ── Date Helpers ────────────────────────────────────────────────────
|
|
565
565
|
function formatDate(d) {
|
|
566
|
-
return formatDateInTimeZone(d,
|
|
566
|
+
return formatDateInTimeZone(d, currentTimeZone());
|
|
567
567
|
}
|
|
568
568
|
function formatTime(d) {
|
|
569
|
-
return formatTimeInTimeZone(d,
|
|
569
|
+
return formatTimeInTimeZone(d, currentTimeZone());
|
|
570
570
|
}
|
|
571
571
|
/** Local-time YYYY-MM-DD (avoids UTC date mismatch late at night). */
|
|
572
572
|
function todayISO() {
|
|
573
|
-
return dateKeyInTimeZone(new Date(),
|
|
573
|
+
return dateKeyInTimeZone(new Date(), currentTimeZone());
|
|
574
574
|
}
|
|
575
575
|
function yesterdayISO() {
|
|
576
|
-
return dateKeyInTimeZone(new Date(Date.now() - 24 * 60 * 60 * 1000),
|
|
576
|
+
return dateKeyInTimeZone(new Date(Date.now() - 24 * 60 * 60 * 1000), currentTimeZone());
|
|
577
577
|
}
|
|
578
578
|
// ── Cron Output Extraction ──────────────────────────────────────────
|
|
579
579
|
/** Autonomous jobs use this sentinel to mean "completed, but do not notify the owner." */
|
|
@@ -1139,7 +1139,7 @@ Large tool outputs blow the context window and rotate your session mid-task —
|
|
|
1139
1139
|
// Skip yesterday's notes and recent conversation summaries for autonomous runs
|
|
1140
1140
|
if (!isAutonomous && !skipAmbientContext) {
|
|
1141
1141
|
if (!retrievalContext) {
|
|
1142
|
-
const hour = hourInTimeZone(new Date(),
|
|
1142
|
+
const hour = hourInTimeZone(new Date(), currentTimeZone());
|
|
1143
1143
|
const mentionsYesterday = this._lastUserMessage?.toLowerCase().includes('yesterday');
|
|
1144
1144
|
if (hour < 12 || mentionsYesterday) {
|
|
1145
1145
|
const yPath = path.join(DAILY_NOTES_DIR, `${yesterdayISO()}.md`);
|
|
@@ -1630,11 +1630,12 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1630
1630
|
const modelLabel = Object.entries(MODELS).find(([, v]) => v === resolvedModel)?.[0] ?? resolvedModel;
|
|
1631
1631
|
const caps = !isAutonomous ? getChannelCapabilities(channel) : null;
|
|
1632
1632
|
const now = new Date();
|
|
1633
|
+
const timeZone = currentTimeZone();
|
|
1633
1634
|
volatileParts.push(`## Current Context
|
|
1634
1635
|
|
|
1635
1636
|
- **Date:** ${formatDate(now)}
|
|
1636
1637
|
- **Time:** ${formatTime(now)}
|
|
1637
|
-
- **Timezone:** ${
|
|
1638
|
+
- **Timezone:** ${timeZone}
|
|
1638
1639
|
- **Channel:** ${channel}${caps ? ` (${formatCapabilities(caps)})` : ''}
|
|
1639
1640
|
- **Model:** ${modelLabel} (${resolvedModel})
|
|
1640
1641
|
- **Vault:** ${vault}
|
|
@@ -3190,13 +3191,16 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3190
3191
|
pending.shift();
|
|
3191
3192
|
this.pendingContext.set(sessionKey, pending);
|
|
3192
3193
|
}
|
|
3194
|
+
if (opts.countExchange) {
|
|
3195
|
+
this.exchangeCounts.set(sessionKey, (this.exchangeCounts.get(sessionKey) ?? 0) + 1);
|
|
3196
|
+
}
|
|
3193
3197
|
this.sessionTimestamps.set(sessionKey, new Date());
|
|
3194
3198
|
this.saveSessions();
|
|
3195
3199
|
// Persist to transcript store
|
|
3196
3200
|
if (this.memoryStore) {
|
|
3197
3201
|
try {
|
|
3198
3202
|
this.memoryStore.saveTurn(sessionKey, 'user', userText);
|
|
3199
|
-
this.memoryStore.saveTurn(sessionKey, 'assistant', assistantText, 'cron');
|
|
3203
|
+
this.memoryStore.saveTurn(sessionKey, 'assistant', assistantText, opts.model ?? 'cron');
|
|
3200
3204
|
}
|
|
3201
3205
|
catch {
|
|
3202
3206
|
// Non-fatal
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* cron-scheduler tick picks up pending tasks within ~3 seconds.
|
|
14
14
|
*
|
|
15
15
|
* Restart safety: on daemon startup, any task left in 'running' is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* marked interrupted (its process is gone). The user can resume it
|
|
17
|
+
* explicitly, which requeues the same prompt without pretending the
|
|
18
|
+
* prior run finished.
|
|
19
19
|
*/
|
|
20
20
|
import type { BackgroundTask } from '../types.js';
|
|
21
21
|
export declare const BACKGROUND_TASK_DIR: string;
|
|
@@ -41,17 +41,29 @@ export declare function listBackgroundTasks(filter?: {
|
|
|
41
41
|
fromAgent?: string;
|
|
42
42
|
}, opts?: BackgroundTaskOptions): BackgroundTask[];
|
|
43
43
|
/** Transition a task to 'running' — daemon picked it up. */
|
|
44
|
-
export declare function markRunning(id: string, opts?: BackgroundTaskOptions
|
|
44
|
+
export declare function markRunning(id: string, opts?: BackgroundTaskOptions, meta?: {
|
|
45
|
+
jobName?: string;
|
|
46
|
+
runId?: string;
|
|
47
|
+
sdkSessionId?: string;
|
|
48
|
+
}): BackgroundTask | null;
|
|
49
|
+
/** Patch non-status metadata on a task. Used for notification bookkeeping. */
|
|
50
|
+
export declare function updateBackgroundTask(id: string, patch: Partial<Omit<BackgroundTask, 'id'>>, opts?: BackgroundTaskOptions): BackgroundTask | null;
|
|
45
51
|
/** Transition to 'done' with final result. */
|
|
46
52
|
export declare function markDone(id: string, result: string, deliverableNote?: string, opts?: BackgroundTaskOptions): BackgroundTask | null;
|
|
47
53
|
/** Transition to 'failed' or 'aborted' with error message. */
|
|
48
|
-
export declare function markFailed(id: string, error: string, reason?: 'failed' | 'aborted', opts?: BackgroundTaskOptions): BackgroundTask | null;
|
|
54
|
+
export declare function markFailed(id: string, error: string, reason?: 'failed' | 'aborted' | 'interrupted', opts?: BackgroundTaskOptions): BackgroundTask | null;
|
|
55
|
+
/**
|
|
56
|
+
* Requeue a task that was interrupted by a daemon restart.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resumeBackgroundTask(id: string, opts?: BackgroundTaskOptions): BackgroundTask | null;
|
|
49
59
|
/**
|
|
50
60
|
* Daemon-restart hygiene: any task still in 'running' must be from a
|
|
51
|
-
* prior daemon process. Mark
|
|
52
|
-
* Returns the count of tasks
|
|
61
|
+
* prior daemon process. Mark it interrupted so it can be resumed
|
|
62
|
+
* explicitly. Returns the count of tasks interrupted.
|
|
53
63
|
*/
|
|
54
|
-
export declare function
|
|
64
|
+
export declare function interruptStaleRunningTasks(opts?: BackgroundTaskOptions): number;
|
|
65
|
+
/** Backward-compatible export for callers/tests using the old name. */
|
|
66
|
+
export declare const abortStaleRunningTasks: typeof interruptStaleRunningTasks;
|
|
55
67
|
/** Delete a task file. Callers should avoid deleting active tasks. */
|
|
56
68
|
export declare function deleteBackgroundTask(id: string, opts?: BackgroundTaskOptions): void;
|
|
57
69
|
/** Backward-compatible test helper alias. */
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* cron-scheduler tick picks up pending tasks within ~3 seconds.
|
|
14
14
|
*
|
|
15
15
|
* Restart safety: on daemon startup, any task left in 'running' is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* marked interrupted (its process is gone). The user can resume it
|
|
17
|
+
* explicitly, which requeues the same prompt without pretending the
|
|
18
|
+
* prior run finished.
|
|
19
19
|
*/
|
|
20
20
|
import { randomBytes } from 'node:crypto';
|
|
21
21
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
@@ -102,7 +102,7 @@ export function listBackgroundTasks(filter = {}, opts) {
|
|
|
102
102
|
return out;
|
|
103
103
|
}
|
|
104
104
|
/** Transition a task to 'running' — daemon picked it up. */
|
|
105
|
-
export function markRunning(id, opts) {
|
|
105
|
+
export function markRunning(id, opts, meta = {}) {
|
|
106
106
|
const task = loadBackgroundTask(id, opts);
|
|
107
107
|
if (!task)
|
|
108
108
|
return null;
|
|
@@ -110,9 +110,26 @@ export function markRunning(id, opts) {
|
|
|
110
110
|
return null;
|
|
111
111
|
task.status = 'running';
|
|
112
112
|
task.startedAt = new Date().toISOString();
|
|
113
|
+
delete task.completedAt;
|
|
114
|
+
delete task.error;
|
|
115
|
+
if (meta.jobName)
|
|
116
|
+
task.jobName = meta.jobName;
|
|
117
|
+
if (meta.runId)
|
|
118
|
+
task.runId = meta.runId;
|
|
119
|
+
if (meta.sdkSessionId)
|
|
120
|
+
task.sdkSessionId = meta.sdkSessionId;
|
|
113
121
|
safeWrite(pathFor(id, opts), task);
|
|
114
122
|
return task;
|
|
115
123
|
}
|
|
124
|
+
/** Patch non-status metadata on a task. Used for notification bookkeeping. */
|
|
125
|
+
export function updateBackgroundTask(id, patch, opts) {
|
|
126
|
+
const task = loadBackgroundTask(id, opts);
|
|
127
|
+
if (!task)
|
|
128
|
+
return null;
|
|
129
|
+
const updated = Object.assign(task, patch, { id: task.id });
|
|
130
|
+
safeWrite(pathFor(id, opts), updated);
|
|
131
|
+
return updated;
|
|
132
|
+
}
|
|
116
133
|
function writeFullResultFile(id, result, opts) {
|
|
117
134
|
if (result.length <= RESULT_TRUNCATE_BYTES)
|
|
118
135
|
return undefined;
|
|
@@ -146,28 +163,48 @@ export function markFailed(id, error, reason = 'failed', opts) {
|
|
|
146
163
|
const task = loadBackgroundTask(id, opts);
|
|
147
164
|
if (!task)
|
|
148
165
|
return null;
|
|
149
|
-
if (task.status === 'done' || task.status === 'failed' || task.status === 'aborted')
|
|
166
|
+
if (task.status === 'done' || task.status === 'failed' || task.status === 'aborted' || task.status === 'interrupted')
|
|
150
167
|
return task;
|
|
151
168
|
task.status = reason;
|
|
152
169
|
task.completedAt = new Date().toISOString();
|
|
170
|
+
if (reason === 'interrupted')
|
|
171
|
+
task.interruptedAt = task.completedAt;
|
|
153
172
|
task.error = error.slice(0, 1000);
|
|
154
173
|
safeWrite(pathFor(id, opts), task);
|
|
155
174
|
return task;
|
|
156
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Requeue a task that was interrupted by a daemon restart.
|
|
178
|
+
*/
|
|
179
|
+
export function resumeBackgroundTask(id, opts) {
|
|
180
|
+
const task = loadBackgroundTask(id, opts);
|
|
181
|
+
if (!task || task.status !== 'interrupted')
|
|
182
|
+
return null;
|
|
183
|
+
task.status = 'pending';
|
|
184
|
+
task.resumedAt = new Date().toISOString();
|
|
185
|
+
task.resumeCount = (task.resumeCount ?? 0) + 1;
|
|
186
|
+
delete task.startedAt;
|
|
187
|
+
delete task.completedAt;
|
|
188
|
+
delete task.error;
|
|
189
|
+
safeWrite(pathFor(id, opts), task);
|
|
190
|
+
return task;
|
|
191
|
+
}
|
|
157
192
|
/**
|
|
158
193
|
* Daemon-restart hygiene: any task still in 'running' must be from a
|
|
159
|
-
* prior daemon process. Mark
|
|
160
|
-
* Returns the count of tasks
|
|
194
|
+
* prior daemon process. Mark it interrupted so it can be resumed
|
|
195
|
+
* explicitly. Returns the count of tasks interrupted.
|
|
161
196
|
*/
|
|
162
|
-
export function
|
|
197
|
+
export function interruptStaleRunningTasks(opts) {
|
|
163
198
|
const stuck = listBackgroundTasks({ status: 'running' }, opts);
|
|
164
|
-
let
|
|
199
|
+
let interrupted = 0;
|
|
165
200
|
for (const t of stuck) {
|
|
166
|
-
markFailed(t.id, 'daemon restarted while task was in flight', '
|
|
167
|
-
|
|
201
|
+
markFailed(t.id, 'daemon restarted while task was in flight', 'interrupted', opts);
|
|
202
|
+
interrupted++;
|
|
168
203
|
}
|
|
169
|
-
return
|
|
204
|
+
return interrupted;
|
|
170
205
|
}
|
|
206
|
+
/** Backward-compatible export for callers/tests using the old name. */
|
|
207
|
+
export const abortStaleRunningTasks = interruptStaleRunningTasks;
|
|
171
208
|
/** Delete a task file. Callers should avoid deleting active tasks. */
|
|
172
209
|
export function deleteBackgroundTask(id, opts) {
|
|
173
210
|
try {
|
package/dist/agent/hooks.js
CHANGED
|
@@ -12,7 +12,7 @@ import fs from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
|
-
import { OWNER_NAME, BASE_DIR,
|
|
15
|
+
import { OWNER_NAME, BASE_DIR, currentTimeZone } from '../config.js';
|
|
16
16
|
import { formatTime24InTimeZone } from '../lib/time.js';
|
|
17
17
|
// ── Shared state ───────────────────────────────────────────────────────
|
|
18
18
|
let heartbeatActive = false;
|
|
@@ -190,7 +190,7 @@ export function clearActiveQueryContext() {
|
|
|
190
190
|
// not by us. setInteractionSource resets it in the relevant transitions.
|
|
191
191
|
}
|
|
192
192
|
export function logToolUse(toolName, toolInput) {
|
|
193
|
-
const timestamp = formatTime24InTimeZone(new Date(),
|
|
193
|
+
const timestamp = formatTime24InTimeZone(new Date(), currentTimeZone());
|
|
194
194
|
const summary = summarizeToolCall(toolName, toolInput);
|
|
195
195
|
const entry = `- \`${timestamp}\` **${toolName}** — ${summary}`;
|
|
196
196
|
auditLog.push(entry);
|
|
@@ -310,13 +310,14 @@ function evaluateSendPolicy(policy, agentSlug, recipientEmail) {
|
|
|
310
310
|
// 3. Business hours check
|
|
311
311
|
if (policy.businessHoursOnly) {
|
|
312
312
|
const now = new Date();
|
|
313
|
-
|
|
313
|
+
const timeZone = currentTimeZone();
|
|
314
|
+
// Use the configured user timezone for business hours checks.
|
|
314
315
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
315
|
-
hour: 'numeric', hour12: false, timeZone
|
|
316
|
+
hour: 'numeric', hour12: false, timeZone,
|
|
316
317
|
});
|
|
317
318
|
const hour = parseInt(formatter.format(now), 10);
|
|
318
319
|
if (hour < 8 || hour >= 18) {
|
|
319
|
-
return { allowed: false, reason: `Outside business hours (8am–6pm ${
|
|
320
|
+
return { allowed: false, reason: `Outside business hours (8am–6pm ${timeZone}).`, policyRef: 'business_hours' };
|
|
320
321
|
}
|
|
321
322
|
}
|
|
322
323
|
// 4. Approval mode check
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import pino from 'pino';
|
|
14
|
-
import { GOALS_DIR, BASE_DIR, MEMORY_DB_PATH, VAULT_DIR,
|
|
14
|
+
import { GOALS_DIR, BASE_DIR, MEMORY_DB_PATH, VAULT_DIR, currentTimeZone } from '../config.js';
|
|
15
15
|
import { listAllGoals } from '../tools/shared.js';
|
|
16
16
|
import { computeBrokenJobs } from '../gateway/failure-monitor.js';
|
|
17
17
|
import { MemoryStore } from '../memory/store.js';
|
|
@@ -24,7 +24,7 @@ const UNACKED_THRESHOLD = 3; // double cooldown after this many ignored
|
|
|
24
24
|
* Check if it's too soon to send another proactive message.
|
|
25
25
|
*/
|
|
26
26
|
export function canSendInsight(state) {
|
|
27
|
-
const today = dateKeyInTimeZone(new Date(),
|
|
27
|
+
const today = dateKeyInTimeZone(new Date(), currentTimeZone());
|
|
28
28
|
// Reset daily count on new day
|
|
29
29
|
if (state.currentDate !== today) {
|
|
30
30
|
state.sentToday = [];
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
* through the canonical runAgent() instead of buildOptions+query.
|
|
15
15
|
*/
|
|
16
16
|
import pino from 'pino';
|
|
17
|
-
import { OWNER_NAME, MODELS, BUDGET,
|
|
17
|
+
import { OWNER_NAME, MODELS, BUDGET, currentTimeZone, } from '../config.js';
|
|
18
18
|
import { formatDateInTimeZone, formatTimeInTimeZone } from '../lib/time.js';
|
|
19
19
|
const OWNER = OWNER_NAME || 'the user';
|
|
20
20
|
function formatDate(d) {
|
|
21
|
-
return formatDateInTimeZone(d,
|
|
21
|
+
return formatDateInTimeZone(d, currentTimeZone());
|
|
22
22
|
}
|
|
23
23
|
function formatTime(d) {
|
|
24
|
-
return formatTimeInTimeZone(d,
|
|
24
|
+
return formatTimeInTimeZone(d, currentTimeZone());
|
|
25
25
|
}
|
|
26
26
|
import { runAgent } from './run-agent.js';
|
|
27
27
|
const logger = pino({ name: 'clementine.run-agent-heartbeat' });
|
|
@@ -35,7 +35,7 @@ export async function runAgentHeartbeat(opts) {
|
|
|
35
35
|
const now = new Date();
|
|
36
36
|
const localTime = formatTime(now);
|
|
37
37
|
const localDate = formatDate(now);
|
|
38
|
-
const tz =
|
|
38
|
+
const tz = currentTimeZone();
|
|
39
39
|
const owner = OWNER;
|
|
40
40
|
const agentName = opts.profile?.name ?? 'personal assistant';
|
|
41
41
|
const promptParts = [
|
|
@@ -75,6 +75,10 @@ export interface RunAgentOptions {
|
|
|
75
75
|
/** Optional explicit allowedTools list. When unset, falls back to a sensible default
|
|
76
76
|
* including Agent (so subagents can be spawned) + core SDK tools + Clementine MCP. */
|
|
77
77
|
allowedTools?: string[];
|
|
78
|
+
/** Extra tools to pre-approve without making their built-in tools visible to
|
|
79
|
+
* the main agent. Useful when the main agent may only call Agent, but the
|
|
80
|
+
* forced subagent still needs pre-approved MCP/Clementine tools. */
|
|
81
|
+
permissionTools?: string[];
|
|
78
82
|
/** SDK permission mode. Defaults to dontAsk so allowedTools is enforceable.
|
|
79
83
|
* Only explicit operator/full-surface paths should request bypassPermissions. */
|
|
80
84
|
permissionMode?: ExecutionPermissionMode;
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -254,6 +254,27 @@ export async function runAgent(prompt, opts) {
|
|
|
254
254
|
clementineServerName: TOOLS_SERVER,
|
|
255
255
|
permissionMode: opts.permissionMode,
|
|
256
256
|
});
|
|
257
|
+
const permissionToolPolicy = opts.permissionTools
|
|
258
|
+
? buildExecutionToolPolicy({
|
|
259
|
+
requestedTools: opts.permissionTools,
|
|
260
|
+
defaultBuiltins: CORE_TOOLS_FOR_AGENT_PARENT,
|
|
261
|
+
mcpServerNames: policyMcpServerNames,
|
|
262
|
+
clementineServerName: TOOLS_SERVER,
|
|
263
|
+
permissionMode: opts.permissionMode,
|
|
264
|
+
})
|
|
265
|
+
: null;
|
|
266
|
+
const sdkAllowedTools = permissionToolPolicy
|
|
267
|
+
? Array.from(new Set([...toolPolicy.allowedTools, ...permissionToolPolicy.allowedTools])).sort()
|
|
268
|
+
: toolPolicy.allowedTools;
|
|
269
|
+
const clementineToolAllowlist = (() => {
|
|
270
|
+
if (!permissionToolPolicy)
|
|
271
|
+
return toolPolicy.clementineToolAllowlist;
|
|
272
|
+
const parts = [toolPolicy.clementineToolAllowlist, permissionToolPolicy.clementineToolAllowlist]
|
|
273
|
+
.flatMap(v => v.split(',').map(s => s.trim()).filter(Boolean));
|
|
274
|
+
if (parts.includes('*'))
|
|
275
|
+
return '*';
|
|
276
|
+
return Array.from(new Set(parts)).sort().join(',');
|
|
277
|
+
})();
|
|
257
278
|
// SDK accepts a Record<string, McpServerConfig> here. We cast on
|
|
258
279
|
// assignment because we mix the always-on Clementine stdio server
|
|
259
280
|
// with caller-supplied servers of various types.
|
|
@@ -267,7 +288,7 @@ export async function runAgent(prompt, opts) {
|
|
|
267
288
|
CLEMENTINE_HOME: BASE_DIR,
|
|
268
289
|
...(opts.profile?.slug ? { CLEMENTINE_TEAM_AGENT: opts.profile.slug } : {}),
|
|
269
290
|
CLEMENTINE_INTERACTION_SOURCE: interactionSourceForSession(opts.sessionKey, source),
|
|
270
|
-
CLEMENTINE_TOOL_ALLOWLIST:
|
|
291
|
+
CLEMENTINE_TOOL_ALLOWLIST: clementineToolAllowlist,
|
|
271
292
|
},
|
|
272
293
|
},
|
|
273
294
|
...baseMcpServers,
|
|
@@ -302,6 +323,20 @@ export async function runAgent(prompt, opts) {
|
|
|
302
323
|
}
|
|
303
324
|
catch { /* never block */ }
|
|
304
325
|
};
|
|
326
|
+
// Durable SDK session store: mirror parent + subagent SDK transcripts into
|
|
327
|
+
// Clementine's SQLite memory store. The chat gateway already persists the
|
|
328
|
+
// SDK session id; this store makes restart resume independent of local JSONL
|
|
329
|
+
// files and keeps session inspection/querying in the same memory backend.
|
|
330
|
+
let sessionStore;
|
|
331
|
+
if (opts.memoryStore) {
|
|
332
|
+
try {
|
|
333
|
+
const { createMemorySessionStore } = await import('./session-store-adapter.js');
|
|
334
|
+
sessionStore = createMemorySessionStore(opts.memoryStore);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
logger.debug({ err }, 'runAgent: SessionStore adapter unavailable (non-fatal)');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
305
340
|
// ── Tool-output guard hooks (1.18.169) ─────────────────────────────
|
|
306
341
|
// Bounds the per-tool-call output that reaches the model so SDK
|
|
307
342
|
// auto-compaction never thrashes on a runaway MCP result. The hook
|
|
@@ -393,8 +428,9 @@ export async function runAgent(prompt, opts) {
|
|
|
393
428
|
// callers can mix stdio + http + sse server shapes.
|
|
394
429
|
mcpServers: mcpServers,
|
|
395
430
|
tools: toolPolicy.builtinTools,
|
|
396
|
-
allowedTools:
|
|
431
|
+
allowedTools: sdkAllowedTools,
|
|
397
432
|
permissionMode: toolPolicy.permissionMode,
|
|
433
|
+
...(sessionStore ? { sessionStore } : {}),
|
|
398
434
|
...(toolPolicy.allowDangerouslySkipPermissions
|
|
399
435
|
? { allowDangerouslySkipPermissions: toolPolicy.allowDangerouslySkipPermissions }
|
|
400
436
|
: {}),
|
|
@@ -432,10 +468,11 @@ export async function runAgent(prompt, opts) {
|
|
|
432
468
|
maxBudgetUsd: maxBudgetUsd ?? 'uncapped',
|
|
433
469
|
agentCount: Object.keys(agents).length,
|
|
434
470
|
allowedToolCount: allowedTools.length,
|
|
435
|
-
sdkAllowedToolCount:
|
|
471
|
+
sdkAllowedToolCount: sdkAllowedTools.length,
|
|
436
472
|
builtinToolCount: toolPolicy.builtinTools.length,
|
|
437
473
|
permissionMode: toolPolicy.permissionMode,
|
|
438
474
|
mcpServerCount: Object.keys(mcpServers).length,
|
|
475
|
+
sessionStore: !!sessionStore,
|
|
439
476
|
}, 'runAgent: starting query');
|
|
440
477
|
// PRD §6 / 1.18.85: path A in-process tap. runId / eventLog / writeEvent
|
|
441
478
|
// are declared earlier (above sdkOptionsRaw) because the tool-output
|
|
@@ -723,7 +760,7 @@ export async function runAgent(prompt, opts) {
|
|
|
723
760
|
...(usage ? { usage } : {}),
|
|
724
761
|
runId,
|
|
725
762
|
permissionMode: toolPolicy.permissionMode,
|
|
726
|
-
allowedToolsApplied:
|
|
763
|
+
allowedToolsApplied: sdkAllowedTools,
|
|
727
764
|
builtinToolsApplied: toolPolicy.builtinTools,
|
|
728
765
|
mcpServersApplied: Object.keys(mcpServers),
|
|
729
766
|
};
|
package/dist/agent/run-skill.js
CHANGED
|
@@ -260,7 +260,7 @@ function resolveAutonomousModel(explicitModel, skillModel) {
|
|
|
260
260
|
* Since the parent is `forceSubagent`'d to this worker, the description
|
|
261
261
|
* mostly serves as transcript context.
|
|
262
262
|
*/
|
|
263
|
-
function buildSkillWorkerAgent(skill, effectiveTools, model, workerMaxTurns) {
|
|
263
|
+
function buildSkillWorkerAgent(skill, renderedProcedure, effectiveTools, model, workerMaxTurns) {
|
|
264
264
|
const def = {
|
|
265
265
|
description: `Executes the "${skill.frontmatter.name}" scheduled skill end-to-end in an isolated context window. ` +
|
|
266
266
|
`Reads any data the skill needs, processes it, performs the skill's described delivery action ` +
|
|
@@ -272,7 +272,7 @@ function buildSkillWorkerAgent(skill, effectiveTools, model, workerMaxTurns) {
|
|
|
272
272
|
`Return a single concise final response describing what happened (e.g., "Sent Discord DM about ` +
|
|
273
273
|
`2 actionable items, ignored 8 spam"). Do not return raw tool output; do not narrate every step. ` +
|
|
274
274
|
`If nothing actionable was found and the procedure says exit silently, return "No action needed."\n\n` +
|
|
275
|
-
`## Procedure\n\n${
|
|
275
|
+
`## Procedure\n\n${renderedProcedure}`,
|
|
276
276
|
tools: effectiveTools,
|
|
277
277
|
// SDK accepts 'sonnet' / 'opus' / 'haiku' tier aliases OR full model
|
|
278
278
|
// IDs. We pass the full ID with [1m] when present; the SDK strips
|
|
@@ -291,7 +291,7 @@ function buildSkillWorkerAgent(skill, effectiveTools, model, workerMaxTurns) {
|
|
|
291
291
|
* return text (typically <2KB). Total parent context per run: ~3KB.
|
|
292
292
|
* Well below any compaction threshold even on a 200K-window model.
|
|
293
293
|
*/
|
|
294
|
-
function buildOrchestratorPrompt(skill
|
|
294
|
+
function buildOrchestratorPrompt(skill) {
|
|
295
295
|
const parts = [
|
|
296
296
|
`## Scheduled Skill Execution`,
|
|
297
297
|
``,
|
|
@@ -306,9 +306,6 @@ function buildOrchestratorPrompt(skill, callerContext) {
|
|
|
306
306
|
``,
|
|
307
307
|
`Do NOT call any other tools directly. The worker handles all data access and delivery.`,
|
|
308
308
|
];
|
|
309
|
-
if (callerContext && callerContext.trim()) {
|
|
310
|
-
parts.push('', '## Caller context (forward this to the worker if relevant)', '', callerContext.trim());
|
|
311
|
-
}
|
|
312
309
|
return parts.join('\n');
|
|
313
310
|
}
|
|
314
311
|
// ── The primitive ─────────────────────────────────────────────────────
|
|
@@ -355,9 +352,10 @@ export async function runSkill(name, options = {}) {
|
|
|
355
352
|
// skill reads, so post-compaction refetch loops are structurally
|
|
356
353
|
// impossible. See shouldAutoDelegate / buildSkillWorkerAgent above.
|
|
357
354
|
const autoDelegate = shouldAutoDelegate(skill, source);
|
|
355
|
+
const renderedSkillPrompt = buildSkillPrompt(skill, options.inputs, options.context);
|
|
358
356
|
const prompt = autoDelegate
|
|
359
|
-
? buildOrchestratorPrompt(skill
|
|
360
|
-
:
|
|
357
|
+
? buildOrchestratorPrompt(skill)
|
|
358
|
+
: renderedSkillPrompt;
|
|
361
359
|
const limits = skill.frontmatter?.clementine?.limits;
|
|
362
360
|
const maxTurns = options.maxTurns ?? limits?.maxTurns;
|
|
363
361
|
const maxBudgetUsd = options.maxBudgetUsd ?? limits?.maxBudgetUsd;
|
|
@@ -411,7 +409,7 @@ export async function runSkill(name, options = {}) {
|
|
|
411
409
|
// Worker gets enough turns to complete bulk work (skill author's
|
|
412
410
|
// maxTurns cap, or 30 as a safe default for triage-class work).
|
|
413
411
|
const workerMaxTurns = (typeof maxTurns === 'number' && maxTurns > 0) ? maxTurns : 30;
|
|
414
|
-
const workerDef = buildSkillWorkerAgent(skill, effectiveTools, effectiveModel, workerMaxTurns);
|
|
412
|
+
const workerDef = buildSkillWorkerAgent(skill, renderedSkillPrompt, effectiveTools, effectiveModel, workerMaxTurns);
|
|
415
413
|
sdkOpts = {
|
|
416
414
|
sessionKey,
|
|
417
415
|
source,
|
|
@@ -419,6 +417,9 @@ export async function runSkill(name, options = {}) {
|
|
|
419
417
|
// the parent's context shape predictable and prevents it from
|
|
420
418
|
// doing data-heavy work itself even if the LLM disagrees.
|
|
421
419
|
allowedTools: ['Agent'],
|
|
420
|
+
// SDK permissions are session-level, so the worker's tools still
|
|
421
|
+
// need to be pre-approved even though the parent only sees Agent.
|
|
422
|
+
permissionTools: ['Agent', ...effectiveTools],
|
|
422
423
|
// Force-routing: SDK wraps the prompt with "Use the skill-worker
|
|
423
424
|
// agent to handle this request" so dispatch is the natural
|
|
424
425
|
// first action.
|
package/dist/agent/team-bus.js
CHANGED
|
@@ -9,7 +9,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } fr
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import pino from 'pino';
|
|
11
11
|
import { listAllGoals } from '../tools/shared.js';
|
|
12
|
-
import {
|
|
12
|
+
import { currentTimeZone } from '../config.js';
|
|
13
13
|
import { dateKeyInTimeZone } from '../lib/time.js';
|
|
14
14
|
const logger = pino({ name: 'clementine.team-bus' });
|
|
15
15
|
/** Max inter-agent message depth before rejection (anti-loop). */
|
|
@@ -512,7 +512,7 @@ export class TeamBus {
|
|
|
512
512
|
* Used for proactive context sharing, not task delegation.
|
|
513
513
|
*/
|
|
514
514
|
async shareContext(fromSlug, toSlug, content) {
|
|
515
|
-
const today = dateKeyInTimeZone(new Date(),
|
|
515
|
+
const today = dateKeyInTimeZone(new Date(), currentTimeZone());
|
|
516
516
|
// Daily cap: max 2 context shares per sender per day
|
|
517
517
|
const tracker = this.contextShareCounts.get(fromSlug);
|
|
518
518
|
if (tracker && tracker.date === today && tracker.count >= 2) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pure functions for resolving {{...}} placeholders in workflow step prompts.
|
|
5
5
|
*/
|
|
6
|
-
import { OWNER_NAME, ASSISTANT_NAME,
|
|
6
|
+
import { OWNER_NAME, ASSISTANT_NAME, currentTimeZone } from '../config.js';
|
|
7
7
|
import { dateKeyInTimeZone, formatTime24InTimeZone } from '../lib/time.js';
|
|
8
8
|
/**
|
|
9
9
|
* Resolve static variables (available before any step executes).
|
|
@@ -11,8 +11,9 @@ import { dateKeyInTimeZone, formatTime24InTimeZone } from '../lib/time.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export function resolveStaticVariables(template, inputs, workflowName) {
|
|
13
13
|
const now = new Date();
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const timezone = currentTimeZone();
|
|
15
|
+
const dateStr = dateKeyInTimeZone(now, timezone);
|
|
16
|
+
const timeStr = formatTime24InTimeZone(now, timezone);
|
|
16
17
|
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
17
18
|
const trimmed = key.trim();
|
|
18
19
|
// {{input.topic}} → inputs['topic']
|
package/dist/cli/cron.js
CHANGED
|
@@ -13,7 +13,7 @@ import cron from 'node-cron';
|
|
|
13
13
|
import matter from 'gray-matter';
|
|
14
14
|
import { HeartbeatScheduler } from '../gateway/heartbeat-scheduler.js';
|
|
15
15
|
import { parseCronJobs, CronRunLog, classifyError } from '../gateway/cron-scheduler.js';
|
|
16
|
-
import {
|
|
16
|
+
import { currentTimeZone } from '../config.js';
|
|
17
17
|
import { hourInTimeZone } from '../lib/time.js';
|
|
18
18
|
const BASE_DIR = process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine');
|
|
19
19
|
const LAST_RUN_FILE = path.join(BASE_DIR, '.cron_last_run.json');
|
|
@@ -533,7 +533,7 @@ export async function cmdHeartbeat() {
|
|
|
533
533
|
const parsed = matter(raw);
|
|
534
534
|
standingInstructions = parsed.content;
|
|
535
535
|
}
|
|
536
|
-
const hour = hourInTimeZone(new Date(),
|
|
536
|
+
const hour = hourInTimeZone(new Date(), currentTimeZone());
|
|
537
537
|
const timeContext = HeartbeatScheduler.getTimeContext(hour);
|
|
538
538
|
console.log('Running one-shot heartbeat...');
|
|
539
539
|
const response = await gateway.handleHeartbeat(standingInstructions, '', timeContext);
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -19,7 +19,7 @@ import { TunnelManager } from './tunnel.js';
|
|
|
19
19
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
20
20
|
import { discoverMcpServers, getClaudeIntegrations, KNOWN_MCP_DESCRIPTIONS } from '../agent/mcp-bridge.js';
|
|
21
21
|
import { buildBuilderEnrichedMessage, builderSessionKey } from '../dashboard/builder/prompt.js';
|
|
22
|
-
import { AGENTS_DIR, MEMORY_FILE, SESSIONS_FILE, TIMEZONE, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
22
|
+
import { AGENTS_DIR, MEMORY_FILE, SESSIONS_FILE, TIMEZONE, applyOneMillionContextRecovery, currentTimeZone, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
23
23
|
import { parseTasks } from '../tools/shared.js';
|
|
24
24
|
// 1.18.160 — also pull parseCronJobs + parseAgentCronJobs so getCronJobs()
|
|
25
25
|
// returns the same merged set the runtime fires (CRON.md + agent CRON +
|
|
@@ -1092,7 +1092,7 @@ function dashboardTimeZone() {
|
|
|
1092
1092
|
catch {
|
|
1093
1093
|
jsonTz = '';
|
|
1094
1094
|
}
|
|
1095
|
-
return resolveTimeZone(process.env.TIMEZONE, readDashboardEnvValue('TIMEZONE'), jsonTz, TIMEZONE);
|
|
1095
|
+
return resolveTimeZone(process.env.TIMEZONE, readDashboardEnvValue('TIMEZONE'), jsonTz, currentTimeZone(), TIMEZONE);
|
|
1096
1096
|
}
|
|
1097
1097
|
function dashboardTimeSnapshot(date = new Date()) {
|
|
1098
1098
|
const timeZone = dashboardTimeZone();
|
|
@@ -9229,7 +9229,16 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
9229
9229
|
}
|
|
9230
9230
|
if (key === 'TIMEZONE') {
|
|
9231
9231
|
process.env.TIMEZONE = value;
|
|
9232
|
-
process.env.TZ =
|
|
9232
|
+
process.env.TZ = currentTimeZone();
|
|
9233
|
+
try {
|
|
9234
|
+
const gw = await getGateway();
|
|
9235
|
+
const sched = gw.cronScheduler;
|
|
9236
|
+
sched?.reloadJobs?.();
|
|
9237
|
+
sched?.reloadWorkflows?.();
|
|
9238
|
+
}
|
|
9239
|
+
catch {
|
|
9240
|
+
// Best-effort: persisted setting still applies on next scheduler start.
|
|
9241
|
+
}
|
|
9233
9242
|
}
|
|
9234
9243
|
res.json({ ok: true, message: `Updated ${key}` });
|
|
9235
9244
|
}
|
|
@@ -9257,7 +9266,16 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
9257
9266
|
}
|
|
9258
9267
|
if (key === 'TIMEZONE') {
|
|
9259
9268
|
delete process.env.TIMEZONE;
|
|
9260
|
-
process.env.TZ =
|
|
9269
|
+
process.env.TZ = currentTimeZone();
|
|
9270
|
+
try {
|
|
9271
|
+
const gw = await getGateway();
|
|
9272
|
+
const sched = gw.cronScheduler;
|
|
9273
|
+
sched?.reloadJobs?.();
|
|
9274
|
+
sched?.reloadWorkflows?.();
|
|
9275
|
+
}
|
|
9276
|
+
catch {
|
|
9277
|
+
// Best-effort: persisted setting still applies on next scheduler start.
|
|
9278
|
+
}
|
|
9261
9279
|
}
|
|
9262
9280
|
res.json({ ok: true, message: `Removed ${key}` });
|
|
9263
9281
|
}
|
|
@@ -25086,6 +25104,7 @@ function settingRequiresDaemonRestart(key) {
|
|
|
25086
25104
|
if (!key) return true;
|
|
25087
25105
|
if (key === 'COMPOSIO_API_KEY' || key === 'COMPOSIO_USER_ID') return false;
|
|
25088
25106
|
if (key.indexOf('ASSISTANT_') === 0) return false;
|
|
25107
|
+
if (key === 'TIMEZONE') return false;
|
|
25089
25108
|
return true;
|
|
25090
25109
|
}
|
|
25091
25110
|
|
|
@@ -25188,8 +25207,7 @@ async function saveTimezoneSetting() {
|
|
|
25188
25207
|
dashboardTimeState.serverIso = new Date().toISOString();
|
|
25189
25208
|
dashboardTimeState.syncedAt = Date.now();
|
|
25190
25209
|
renderDashboardTimeWidgets();
|
|
25191
|
-
|
|
25192
|
-
if (status) { status.textContent = 'Saved'; status.style.color = 'var(--green)'; setTimeout(function(){ status.textContent = ''; }, 2000); }
|
|
25210
|
+
if (status) { status.textContent = 'Saved; schedules reloaded'; status.style.color = 'var(--green)'; setTimeout(function(){ status.textContent = ''; }, 2500); }
|
|
25193
25211
|
refreshHomeDigest();
|
|
25194
25212
|
} else if (status) {
|
|
25195
25213
|
status.textContent = 'Error';
|
|
@@ -8,7 +8,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import matter from 'gray-matter';
|
|
10
10
|
import { listAllGoals } from '../../tools/shared.js';
|
|
11
|
-
import {
|
|
11
|
+
import { currentTimeZone } from '../../config.js';
|
|
12
12
|
import { dateKeyInTimeZone, formatDateInTimeZone } from '../../lib/time.js';
|
|
13
13
|
export function getDigestPrefs(prefsFile) {
|
|
14
14
|
const defaults = {
|
|
@@ -78,8 +78,9 @@ export function digestRouter(deps) {
|
|
|
78
78
|
const prefs = getDigestPrefs(prefsFile);
|
|
79
79
|
const secs = (prefs.sections || {});
|
|
80
80
|
const now = new Date();
|
|
81
|
-
const
|
|
82
|
-
const
|
|
81
|
+
const timezone = currentTimeZone();
|
|
82
|
+
const dateStr = formatDateInTimeZone(now, timezone);
|
|
83
|
+
const todayKey = dateKeyInTimeZone(now, timezone);
|
|
83
84
|
const sections = {};
|
|
84
85
|
let officeSummary = '';
|
|
85
86
|
try {
|