kimaki 0.24.0 → 0.26.0
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-model.e2e.test.js +135 -3
- package/dist/analytics.test.js +30 -1
- package/dist/cli-commands/send.js +13 -3
- package/dist/cli-commands/task.js +26 -7
- package/dist/cli-runner.js +66 -70
- package/dist/cli-runner.test.js +10 -1
- package/dist/cli.js +5 -0
- package/dist/cloudflare-transcription.js +56 -0
- package/dist/commands/agent.js +87 -74
- package/dist/commands/model-variant.js +17 -1
- package/dist/commands/model.js +18 -0
- package/dist/commands/multioauth.js +169 -1
- package/dist/commands/queue.js +64 -0
- package/dist/commands/tasks.js +6 -0
- package/dist/commands/unset-model.js +5 -20
- package/dist/config.js +9 -0
- package/dist/database.js +65 -0
- package/dist/discord-bot.js +8 -6
- package/dist/discord-command-registration.js +17 -12
- package/dist/external-directory-permissions.e2e.test.js +82 -0
- package/dist/external-directory-project-deny.e2e.test.js +68 -0
- package/dist/format-tables.js +53 -5
- package/dist/format-tables.test.js +121 -0
- package/dist/interaction-handler.js +8 -6
- package/dist/ipc-tools-plugin.js +4 -3
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/message-formatting.js +84 -5
- package/dist/message-formatting.test.js +160 -2
- package/dist/message-preprocessing.js +2 -25
- package/dist/oauth-rotation-shared.js +7 -1
- package/dist/openai-auth-state.js +20 -0
- package/dist/opencode.js +126 -96
- package/dist/queue-advanced-e2e-setup.js +63 -27
- package/dist/queue-advanced-permissions-typing.e2e.test.js +4 -0
- package/dist/schema.js +19 -0
- package/dist/session-handler/event-stream-state.js +147 -0
- package/dist/session-handler/event-stream-state.test.js +437 -1
- package/dist/session-handler/thread-session-runtime.js +95 -57
- package/dist/store.js +1 -0
- package/dist/subagent-rate-limit-plugin.js +13 -2
- package/dist/system-message.js +18 -4
- package/dist/system-message.test.js +28 -4
- package/dist/task-runner.js +143 -12
- package/dist/task-schedule.js +12 -0
- package/dist/task-schedule.test.js +149 -2
- package/dist/voice-handler.js +37 -11
- package/dist/xai-auth-plugin.js +237 -0
- package/dist/xai-auth-state.js +233 -0
- package/dist/xai-auth-state.test.js +183 -0
- package/package.json +6 -6
- package/skills/profano/SKILL.md +5 -3
- package/skills/spiceflow/SKILL.md +24 -1
- package/src/agent-model.e2e.test.ts +172 -4
- package/src/analytics.test.ts +30 -1
- package/src/analytics.ts +1 -0
- package/src/cli-commands/send.ts +22 -3
- package/src/cli-commands/task.ts +32 -19
- package/src/cli-runner.test.ts +16 -1
- package/src/cli-runner.ts +88 -89
- package/src/cli.ts +11 -0
- package/src/cloudflare-transcription.ts +76 -0
- package/src/commands/agent.ts +120 -84
- package/src/commands/model-variant.ts +26 -0
- package/src/commands/model.ts +25 -0
- package/src/commands/multioauth.ts +195 -2
- package/src/commands/queue.ts +83 -0
- package/src/commands/tasks.ts +6 -0
- package/src/commands/unset-model.ts +5 -21
- package/src/config.ts +10 -0
- package/src/database.ts +124 -0
- package/src/discord-bot.ts +7 -6
- package/src/discord-command-registration.ts +22 -15
- package/src/external-directory-permissions.e2e.test.ts +99 -0
- package/src/external-directory-project-deny.e2e.test.ts +82 -0
- package/src/format-tables.test.ts +126 -0
- package/src/format-tables.ts +68 -5
- package/src/interaction-handler.ts +10 -5
- package/src/ipc-tools-plugin.ts +4 -3
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/message-formatting.test.ts +197 -2
- package/src/message-formatting.ts +98 -5
- package/src/message-preprocessing.ts +2 -37
- package/src/oauth-rotation-shared.ts +6 -1
- package/src/openai-auth-state.ts +19 -0
- package/src/opencode.ts +132 -111
- package/src/queue-advanced-e2e-setup.ts +78 -29
- package/src/queue-advanced-permissions-typing.e2e.test.ts +4 -0
- package/src/schema.sql +15 -0
- package/src/schema.ts +20 -0
- package/src/session-handler/event-stream-state.test.ts +490 -0
- package/src/session-handler/event-stream-state.ts +210 -0
- package/src/session-handler/model-utils.ts +1 -0
- package/src/session-handler/thread-runtime-state.ts +0 -4
- package/src/session-handler/thread-session-runtime.ts +108 -69
- package/src/store.ts +10 -0
- package/src/subagent-rate-limit-plugin.ts +12 -2
- package/src/system-message.test.ts +29 -4
- package/src/system-message.ts +20 -4
- package/src/task-runner.ts +168 -11
- package/src/task-schedule.test.ts +172 -2
- package/src/task-schedule.ts +23 -2
- package/src/voice-handler.ts +37 -10
- package/src/xai-auth-plugin.ts +302 -0
- package/src/xai-auth-state.test.ts +224 -0
- package/src/xai-auth-state.ts +293 -0
- package/dist/channel-reference-permissions.e2e.test.js +0 -87
- package/dist/commands/add-dir.js +0 -169
- package/dist/commands/add-dir.test.js +0 -126
- package/src/channel-reference-permissions.e2e.test.ts +0 -104
- package/src/commands/add-dir.test.ts +0 -154
- package/src/commands/add-dir.ts +0 -244
|
@@ -16,8 +16,8 @@ import { isAbortError } from '../utils.js';
|
|
|
16
16
|
import { registerEventListener, unregisterEventListener, waitForGlobalEventListener, } from './global-event-listener.js';
|
|
17
17
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
18
18
|
import { sendThreadMessage, SILENT_MESSAGE_FLAGS, NOTIFY_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
19
|
-
import { formatPart } from '../message-formatting.js';
|
|
20
|
-
import { getChannelVerbosity, getPartMessageIds, getDb, setPartMessage, getThreadSession, setThreadSession, getThreadParentSessionId, setThreadParentSessionId, getThreadWorktreeOrWorkspace, setSessionAgent, clearSessionModel, getVariantCascade, setSessionStartSource, appendSessionEventsSinceLastTimestamp, getSessionEventSnapshot, } from '../database.js';
|
|
19
|
+
import { formatPart, formatTaskToolTitle } from '../message-formatting.js';
|
|
20
|
+
import { getChannelVerbosity, getPartMessageIds, getDb, setPartMessage, getThreadSession, setThreadSession, getThreadParentSessionId, setThreadParentSessionId, getThreadWorktreeOrWorkspace, setSessionAgent, clearSessionModel, getVariantCascade, setSessionStartSource, completeScheduledTaskRunsForSession, failScheduledTaskRunsForSession, startScheduledTaskRunSession, appendSessionEventsSinceLastTimestamp, getSessionEventSnapshot, } from '../database.js';
|
|
21
21
|
import * as orm from 'drizzle-orm';
|
|
22
22
|
import * as schema from '../schema.js';
|
|
23
23
|
import { showPermissionButtons, addPermissionRequestToContext, arePatternsCoveredBy, pendingPermissionContexts, } from '../commands/permissions.js';
|
|
@@ -30,7 +30,7 @@ import { getDataDir } from '../config.js';
|
|
|
30
30
|
import { trackEvent, } from '../analytics.js';
|
|
31
31
|
import { resolveValidatedAgentPreference } from './agent-utils.js';
|
|
32
32
|
import { appendOpencodeSessionEventLog, getOpencodeEventSessionId, isOpencodeSessionEventLogEnabled, } from './opencode-session-event-log.js';
|
|
33
|
-
import { doesLatestUserTurnHaveNaturalCompletion, didQuestionQueueHandoffSinceLatestQuestionAsked, getAssistantMessageIdsForLatestUserTurn, getCurrentTurnStartTime, isSessionBusy, getLatestRunInfo, getDerivedSubtaskIndex, getDerivedSubtaskAgentType, getLatestAssistantMessageIdForLatestUserTurn, hasAssistantMessageCompletedBefore, isAssistantMessageInLatestUserTurn, isAssistantMessageNaturalCompletion, } from './event-stream-state.js';
|
|
33
|
+
import { doesLatestUserTurnHaveNaturalCompletion, didQuestionQueueHandoffSinceLatestQuestionAsked, getAssistantMessageIdsForLatestUserTurn, getCurrentTurnStartTime, isSessionBusy, getLatestRunInfo, getIdleTokenUsageDelta, getDerivedSubtaskIndex, getDerivedSubtaskAgentType, getLatestAssistantMessageIdForLatestUserTurn, hasAssistantMessageCompletedBefore, isAssistantMessageInLatestUserTurn, isAssistantMessageNaturalCompletion, } from './event-stream-state.js';
|
|
34
34
|
// Track multiple pending permissions per thread (keyed by permission ID).
|
|
35
35
|
// OpenCode handles blocking/sequencing — we just need to track all pending
|
|
36
36
|
// permissions to avoid duplicates and properly clean up on reply/teardown.
|
|
@@ -949,9 +949,20 @@ export class ThreadSessionRuntime {
|
|
|
949
949
|
await this.handlePartUpdated(event.properties.part);
|
|
950
950
|
break;
|
|
951
951
|
case 'session.idle':
|
|
952
|
+
await completeScheduledTaskRunsForSession(event.properties.sessionID);
|
|
952
953
|
await this.handleSessionIdle(event.properties.sessionID);
|
|
953
954
|
break;
|
|
954
955
|
case 'session.error':
|
|
956
|
+
if (event.properties.sessionID) {
|
|
957
|
+
const sessionError = event.properties.error;
|
|
958
|
+
const errorMessage = sessionError && typeof sessionError === 'object'
|
|
959
|
+
? String(sessionError.data?.message || sessionError.name || 'Session failed')
|
|
960
|
+
: 'Session failed';
|
|
961
|
+
await failScheduledTaskRunsForSession({
|
|
962
|
+
sessionId: event.properties.sessionID,
|
|
963
|
+
error: errorMessage,
|
|
964
|
+
});
|
|
965
|
+
}
|
|
955
966
|
await this.handleSessionError(event.properties);
|
|
956
967
|
break;
|
|
957
968
|
case 'permission.asked':
|
|
@@ -1433,6 +1444,35 @@ export class ThreadSessionRuntime {
|
|
|
1433
1444
|
this.ensureTypingNow();
|
|
1434
1445
|
return;
|
|
1435
1446
|
}
|
|
1447
|
+
if (part.type === 'tool' &&
|
|
1448
|
+
part.tool === 'task' &&
|
|
1449
|
+
!this.state?.sentPartIds.has(part.id)) {
|
|
1450
|
+
const taskDisplay = formatTaskToolTitle(part);
|
|
1451
|
+
if (taskDisplay && (await this.getVerbosity()) !== 'text_only') {
|
|
1452
|
+
await this.flushBufferedParts({
|
|
1453
|
+
messageID: part.messageID,
|
|
1454
|
+
force: true,
|
|
1455
|
+
skipPartId: part.id,
|
|
1456
|
+
});
|
|
1457
|
+
threadState.updateThread(this.threadId, (t) => {
|
|
1458
|
+
const newIds = new Set(t.sentPartIds);
|
|
1459
|
+
newIds.add(part.id);
|
|
1460
|
+
return { ...t, sentPartIds: newIds };
|
|
1461
|
+
});
|
|
1462
|
+
const sendResult = await sendThreadMessage(this.thread, taskDisplay + '\n\n')
|
|
1463
|
+
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
1464
|
+
if (sendResult instanceof Error) {
|
|
1465
|
+
threadState.updateThread(this.threadId, (t) => {
|
|
1466
|
+
const newIds = new Set(t.sentPartIds);
|
|
1467
|
+
newIds.delete(part.id);
|
|
1468
|
+
return { ...t, sentPartIds: newIds };
|
|
1469
|
+
});
|
|
1470
|
+
discordLogger.error(`ERROR: Failed to send task part ${part.id}:`, sendResult);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
await setPartMessage({ partId: part.id, messageId: sendResult.id, threadId: this.thread.id });
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1436
1476
|
if (part.type === 'tool' && part.state.status === 'running') {
|
|
1437
1477
|
await this.flushBufferedParts({
|
|
1438
1478
|
messageID: part.messageID,
|
|
@@ -1440,40 +1480,6 @@ export class ThreadSessionRuntime {
|
|
|
1440
1480
|
skipPartId: part.id,
|
|
1441
1481
|
});
|
|
1442
1482
|
await this.sendPartMessage({ part });
|
|
1443
|
-
// Track task tool spawning subtask sessions
|
|
1444
|
-
if (part.tool === 'task' && !this.state?.sentPartIds.has(part.id)) {
|
|
1445
|
-
const description = typeof part.state.input?.description === 'string'
|
|
1446
|
-
? part.state.input.description
|
|
1447
|
-
: '';
|
|
1448
|
-
const agent = typeof part.state.input?.subagent_type === 'string'
|
|
1449
|
-
? part.state.input.subagent_type
|
|
1450
|
-
: 'task';
|
|
1451
|
-
const childSessionId = typeof part.state.metadata?.sessionId === 'string'
|
|
1452
|
-
? part.state.metadata.sessionId
|
|
1453
|
-
: '';
|
|
1454
|
-
if (description && childSessionId) {
|
|
1455
|
-
if ((await this.getVerbosity()) !== 'text_only') {
|
|
1456
|
-
const taskDisplay = `┣ ${agent} **${description}**`;
|
|
1457
|
-
threadState.updateThread(this.threadId, (t) => {
|
|
1458
|
-
const newIds = new Set(t.sentPartIds);
|
|
1459
|
-
newIds.add(part.id);
|
|
1460
|
-
return { ...t, sentPartIds: newIds };
|
|
1461
|
-
});
|
|
1462
|
-
const sendResult = await sendThreadMessage(this.thread, taskDisplay + '\n\n')
|
|
1463
|
-
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
1464
|
-
if (sendResult instanceof Error) {
|
|
1465
|
-
threadState.updateThread(this.threadId, (t) => {
|
|
1466
|
-
const newIds = new Set(t.sentPartIds);
|
|
1467
|
-
newIds.delete(part.id);
|
|
1468
|
-
return { ...t, sentPartIds: newIds };
|
|
1469
|
-
});
|
|
1470
|
-
discordLogger.error(`ERROR: Failed to send task part ${part.id}:`, sendResult);
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
await setPartMessage({ partId: part.id, messageId: sendResult.id, threadId: this.thread.id });
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
1483
|
return;
|
|
1478
1484
|
}
|
|
1479
1485
|
// Action buttons tool handler
|
|
@@ -1637,7 +1643,47 @@ export class ThreadSessionRuntime {
|
|
|
1637
1643
|
await setPartMessage({ partId: part.id, messageId: sendResult.id, threadId: this.thread.id });
|
|
1638
1644
|
this.requestTypingRepulse();
|
|
1639
1645
|
}
|
|
1646
|
+
trackIdleTokenUsage(idleSessionId) {
|
|
1647
|
+
let idleEventIndex;
|
|
1648
|
+
for (let i = this.eventBuffer.length - 1; i >= 0; i--) {
|
|
1649
|
+
const event = this.eventBuffer[i]?.event;
|
|
1650
|
+
if (event?.type === 'session.idle' && event.properties.sessionID === idleSessionId) {
|
|
1651
|
+
idleEventIndex = i;
|
|
1652
|
+
break;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (idleEventIndex === undefined) {
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const usage = getIdleTokenUsageDelta({
|
|
1659
|
+
events: this.eventBuffer,
|
|
1660
|
+
sessionId: idleSessionId,
|
|
1661
|
+
idleEventIndex,
|
|
1662
|
+
});
|
|
1663
|
+
if (!usage) {
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const properties = {
|
|
1667
|
+
tokens_input: usage.input,
|
|
1668
|
+
tokens_output: usage.output,
|
|
1669
|
+
tokens_reasoning: usage.reasoning,
|
|
1670
|
+
tokens_cache_read: usage.cacheRead,
|
|
1671
|
+
tokens_cache_write: usage.cacheWrite,
|
|
1672
|
+
tokens_total: usage.total,
|
|
1673
|
+
cost: usage.cost,
|
|
1674
|
+
assistant_message_count: usage.assistantMessageCount,
|
|
1675
|
+
is_subagent: Boolean(this.getSubtaskInfoForSession(idleSessionId)),
|
|
1676
|
+
};
|
|
1677
|
+
if (usage.model) {
|
|
1678
|
+
properties.model = usage.model;
|
|
1679
|
+
}
|
|
1680
|
+
if (usage.providerID) {
|
|
1681
|
+
properties.provider = usage.providerID;
|
|
1682
|
+
}
|
|
1683
|
+
trackEvent('tokens_used', properties);
|
|
1684
|
+
}
|
|
1640
1685
|
async handleSessionIdle(idleSessionId) {
|
|
1686
|
+
this.trackIdleTokenUsage(idleSessionId);
|
|
1641
1687
|
const sessionId = this.state?.sessionId;
|
|
1642
1688
|
// ── Subtask idle ──────────────────────────────────────────
|
|
1643
1689
|
const subtask = this.getSubtaskInfoForSession(idleSessionId);
|
|
@@ -2122,7 +2168,6 @@ export class ThreadSessionRuntime {
|
|
|
2122
2168
|
prompt: input.prompt,
|
|
2123
2169
|
agent: input.agent,
|
|
2124
2170
|
permissions: input.permissions,
|
|
2125
|
-
permissionRules: input.permissionRules,
|
|
2126
2171
|
injectionGuardPatterns: input.injectionGuardPatterns,
|
|
2127
2172
|
sessionStartScheduleKind: input.sessionStartSource?.scheduleKind,
|
|
2128
2173
|
sessionStartScheduledTaskId: input.sessionStartSource?.scheduledTaskId,
|
|
@@ -2137,7 +2182,6 @@ export class ThreadSessionRuntime {
|
|
|
2137
2182
|
sessionId: session.id,
|
|
2138
2183
|
createdNewSession,
|
|
2139
2184
|
permissions: input.permissions,
|
|
2140
|
-
permissionRules: input.permissionRules,
|
|
2141
2185
|
});
|
|
2142
2186
|
if (updatePermissionsResult instanceof Error) {
|
|
2143
2187
|
await cleanupOnError(`Failed to update session permissions: ${updatePermissionsResult.message}`);
|
|
@@ -2333,6 +2377,13 @@ export class ThreadSessionRuntime {
|
|
|
2333
2377
|
await cleanupOnError(`✗ OpenCode API error: ${errorMessage}`);
|
|
2334
2378
|
return;
|
|
2335
2379
|
}
|
|
2380
|
+
if (input.sessionStartSource?.scheduledTaskRunId) {
|
|
2381
|
+
await startScheduledTaskRunSession({
|
|
2382
|
+
runId: input.sessionStartSource.scheduledTaskRunId,
|
|
2383
|
+
sessionId: session.id,
|
|
2384
|
+
projectDirectory: this.sdkDirectory,
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2336
2387
|
logger.log(`[INGRESS] promptAsync accepted by opencode queue sessionId=${session.id} threadId=${this.threadId}`);
|
|
2337
2388
|
if (!input.noReply) {
|
|
2338
2389
|
trackTurnStarted({
|
|
@@ -2369,7 +2420,6 @@ export class ThreadSessionRuntime {
|
|
|
2369
2420
|
agent: input.agent,
|
|
2370
2421
|
model: input.model,
|
|
2371
2422
|
permissions: input.permissions,
|
|
2372
|
-
permissionRules: input.permissionRules,
|
|
2373
2423
|
injectionGuardPatterns: input.injectionGuardPatterns,
|
|
2374
2424
|
parentSessionId: input.parentSessionId,
|
|
2375
2425
|
sourceMessageId: input.sourceMessageId,
|
|
@@ -2505,10 +2555,6 @@ export class ThreadSessionRuntime {
|
|
|
2505
2555
|
// no explicit agent was already set (CLI --agent flag wins).
|
|
2506
2556
|
agent: input.agent || result.agent,
|
|
2507
2557
|
repliedMessage: result.repliedMessage,
|
|
2508
|
-
permissionRules: [
|
|
2509
|
-
...(input.permissionRules ?? []),
|
|
2510
|
-
...(result.permissionRules ?? []),
|
|
2511
|
-
],
|
|
2512
2558
|
preprocess: undefined,
|
|
2513
2559
|
});
|
|
2514
2560
|
const hasPromptText = resolvedInput.prompt.trim().length > 0;
|
|
@@ -2753,7 +2799,6 @@ export class ThreadSessionRuntime {
|
|
|
2753
2799
|
prompt: input.prompt,
|
|
2754
2800
|
agent: input.agent,
|
|
2755
2801
|
permissions: input.permissions,
|
|
2756
|
-
permissionRules: input.permissionRules,
|
|
2757
2802
|
injectionGuardPatterns: input.injectionGuardPatterns,
|
|
2758
2803
|
sessionStartScheduleKind: input.sessionStartScheduleKind,
|
|
2759
2804
|
sessionStartScheduledTaskId: input.sessionStartScheduledTaskId,
|
|
@@ -2772,7 +2817,6 @@ export class ThreadSessionRuntime {
|
|
|
2772
2817
|
sessionId: session.id,
|
|
2773
2818
|
createdNewSession,
|
|
2774
2819
|
permissions: input.permissions,
|
|
2775
|
-
permissionRules: input.permissionRules,
|
|
2776
2820
|
});
|
|
2777
2821
|
if (updatePermissionsResult instanceof Error) {
|
|
2778
2822
|
this.stopTyping();
|
|
@@ -3132,14 +3176,11 @@ export class ThreadSessionRuntime {
|
|
|
3132
3176
|
}
|
|
3133
3177
|
// ── Session Ensure ──────────────────────────────────────────
|
|
3134
3178
|
// Creates or reuses the OpenCode session for this thread.
|
|
3135
|
-
async updateExistingSessionPermissions({ client, sessionId, createdNewSession, permissions,
|
|
3179
|
+
async updateExistingSessionPermissions({ client, sessionId, createdNewSession, permissions, }) {
|
|
3136
3180
|
if (createdNewSession) {
|
|
3137
3181
|
return null;
|
|
3138
3182
|
}
|
|
3139
|
-
const rules = [
|
|
3140
|
-
...(permissionRules ?? []),
|
|
3141
|
-
...parsePermissionRules(permissions ?? []),
|
|
3142
|
-
];
|
|
3183
|
+
const rules = parsePermissionRules(permissions ?? []);
|
|
3143
3184
|
if (rules.length === 0) {
|
|
3144
3185
|
return null;
|
|
3145
3186
|
}
|
|
@@ -3154,7 +3195,7 @@ export class ThreadSessionRuntime {
|
|
|
3154
3195
|
}
|
|
3155
3196
|
return null;
|
|
3156
3197
|
}
|
|
3157
|
-
async ensureSession({ prompt, agent, permissions,
|
|
3198
|
+
async ensureSession({ prompt, agent, permissions, injectionGuardPatterns, sessionStartScheduleKind, sessionStartScheduledTaskId, }) {
|
|
3158
3199
|
const directory = this.sdkDirectory;
|
|
3159
3200
|
// Resolve worktree info for server initialization
|
|
3160
3201
|
const workspaceInfo = await getThreadWorktreeOrWorkspace(this.thread.id);
|
|
@@ -3195,10 +3236,8 @@ export class ThreadSessionRuntime {
|
|
|
3195
3236
|
}
|
|
3196
3237
|
}
|
|
3197
3238
|
if (!session) {
|
|
3198
|
-
// Pass per-session external_directory permissions
|
|
3199
|
-
//
|
|
3200
|
-
// without prompts. These override the server-level 'ask' default via
|
|
3201
|
-
// opencode's findLast() rule evaluation.
|
|
3239
|
+
// Pass per-session external_directory permissions. By default this is a
|
|
3240
|
+
// single allow-everything rule plus the worktree-origin deny rule.
|
|
3202
3241
|
// CLI --permission rules are appended after base rules so they win
|
|
3203
3242
|
// via opencode's findLast() evaluation.
|
|
3204
3243
|
const sessionPermissions = [
|
|
@@ -3206,7 +3245,6 @@ export class ThreadSessionRuntime {
|
|
|
3206
3245
|
directory: this.sdkDirectory,
|
|
3207
3246
|
originalRepoDirectory,
|
|
3208
3247
|
}),
|
|
3209
|
-
...(permissionRules ?? []),
|
|
3210
3248
|
...parsePermissionRules(permissions ?? []),
|
|
3211
3249
|
];
|
|
3212
3250
|
// Omit title so OpenCode auto-generates a summary from the conversation
|
package/dist/store.js
CHANGED
|
@@ -10,6 +10,9 @@ const RATE_LIMIT_TEXT_PATTERNS = [
|
|
|
10
10
|
'retry after',
|
|
11
11
|
'too many requests',
|
|
12
12
|
'quota exceeded',
|
|
13
|
+
'balance exhausted',
|
|
14
|
+
'spending-limit',
|
|
15
|
+
'run out of credits',
|
|
13
16
|
];
|
|
14
17
|
function isRateLimitText(text) {
|
|
15
18
|
if (!text) {
|
|
@@ -66,7 +69,7 @@ function extractRateLimitReason(event) {
|
|
|
66
69
|
}
|
|
67
70
|
if (event.type === 'message.part.updated' && event.properties.part.type === 'retry') {
|
|
68
71
|
const retryError = event.properties.part.error;
|
|
69
|
-
if (retryError.data.statusCode === 429) {
|
|
72
|
+
if (retryError.data.statusCode === 429 || retryError.data.statusCode === 402) {
|
|
70
73
|
return retryError.data.message;
|
|
71
74
|
}
|
|
72
75
|
if (isRateLimitText(retryError.data.responseBody)) {
|
|
@@ -90,9 +93,17 @@ function extractRateLimitReason(event) {
|
|
|
90
93
|
if (!apiError) {
|
|
91
94
|
return undefined;
|
|
92
95
|
}
|
|
93
|
-
if (apiError.statusCode === 429) {
|
|
96
|
+
if (apiError.statusCode === 429 || apiError.statusCode === 402) {
|
|
94
97
|
return apiError.message;
|
|
95
98
|
}
|
|
99
|
+
// xAI returns 403 with spending-limit or run out of credits text.
|
|
100
|
+
// Only treat 403 as a usage limit if the body matches specific patterns,
|
|
101
|
+
// not all 403s (which can be auth errors handled elsewhere).
|
|
102
|
+
if (apiError.statusCode === 403) {
|
|
103
|
+
const errorText = [apiError.message, apiError.responseBody].filter(Boolean).join(' ');
|
|
104
|
+
if (isRateLimitText(errorText))
|
|
105
|
+
return apiError.message;
|
|
106
|
+
}
|
|
96
107
|
if (isRateLimitText(apiError.responseBody)) {
|
|
97
108
|
return apiError.responseBody;
|
|
98
109
|
}
|
package/dist/system-message.js
CHANGED
|
@@ -374,7 +374,8 @@ see --help for options like voice, speed, etc.
|
|
|
374
374
|
|
|
375
375
|
## requesting files from the user
|
|
376
376
|
|
|
377
|
-
To ask the user to upload files from their device, use
|
|
377
|
+
To ask the user to upload files from their device, use \`kimaki_file_upload\`. This shows a native file picker dialog in Discord. The files are downloaded to the project's \`uploads/\` directory and the tool returns the local file paths.
|
|
378
|
+
You MUST call \`kimaki_file_upload\` LAST, after ALL text.
|
|
378
379
|
|
|
379
380
|
## archiving the current thread
|
|
380
381
|
|
|
@@ -492,6 +493,10 @@ Use \`--send-at\` to schedule a one-time or recurring task:
|
|
|
492
493
|
kimaki send --channel ${channelId} --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent>${parentSessionArg}${userArg}
|
|
493
494
|
kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}${userArg}
|
|
494
495
|
|
|
496
|
+
Use \`--pre-run '<command>'\` to check whether a scheduled task should start. Kimaki runs the command in the project directory. Exit code 0 starts the session and appends stdout to the prompt. Any other exit code skips that occurrence. Command output is written to the Kimaki log.
|
|
497
|
+
|
|
498
|
+
Scheduled tasks do not overlap by default. Add \`--allow-concurrency\` only when concurrent sessions from the same task are safe.
|
|
499
|
+
|
|
495
500
|
**ALWAYS pass \`--user\` when scheduling a task.** Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member, so the task can fire completely unnoticed if the user never joined the thread or already left it. This applies to \`--channel\` and \`--thread\` scheduling alike.
|
|
496
501
|
|
|
497
502
|
ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
|
|
@@ -501,6 +506,8 @@ When the user specifies a time without a timezone, ask them to confirm their tim
|
|
|
501
506
|
- \`--notify-only\` to create a reminder thread without auto-starting a session
|
|
502
507
|
- \`--worktree\` to create the scheduled thread as a worktree session (only if the user explicitly asks for a worktree)
|
|
503
508
|
- \`--agent\` and \`--model\` to control scheduled session behavior
|
|
509
|
+
- \`--pre-run\` to start only when a project command exits with code 0
|
|
510
|
+
- \`--allow-concurrency\` to permit overlapping runs from the same task
|
|
504
511
|
- \`--parent-session\` to pass this session as parent of the scheduled child
|
|
505
512
|
- \`--user\` to add a specific user to the scheduled thread (always pass this)
|
|
506
513
|
|
|
@@ -534,7 +541,7 @@ Notification strategy:
|
|
|
534
541
|
Manage scheduled tasks with:
|
|
535
542
|
|
|
536
543
|
kimaki task list
|
|
537
|
-
kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
|
|
544
|
+
kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--pre-run "command"] [--allow-concurrency true|false] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
|
|
538
545
|
kimaki task delete <id>
|
|
539
546
|
|
|
540
547
|
\`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Fix it with \`kimaki task edit <id> --user '<discord-user-id>'\` instead of deleting and recreating the task. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
|
|
@@ -827,9 +834,16 @@ Only ask questions when the request is genuinely ambiguous with multiple valid a
|
|
|
827
834
|
|
|
828
835
|
## ending conversations with options
|
|
829
836
|
|
|
830
|
-
|
|
837
|
+
You MUST write ALL user-visible text FIRST.
|
|
838
|
+
You MUST call \`question\` LAST, after ALL text parts.
|
|
839
|
+
NEVER call \`question\` before your text. Discord will hide the message.
|
|
840
|
+
|
|
841
|
+
The same rule applies to \`kimaki_action_buttons\` and \`kimaki_file_upload\`.
|
|
842
|
+
You MUST call them LAST, after ALL text.
|
|
843
|
+
|
|
844
|
+
ALWAYS use \`question\` when you ask the user a question. Do not write a numbered list in plain text.
|
|
831
845
|
|
|
832
|
-
IMPORTANT: Do NOT use
|
|
846
|
+
IMPORTANT: Do NOT use \`question\` to ask permission before doing work. Do the work first, then offer follow-ups.
|
|
833
847
|
|
|
834
848
|
Examples:
|
|
835
849
|
- After completing edits: offer "Commit changes?"
|
|
@@ -24,6 +24,16 @@ describe('system-message', () => {
|
|
|
24
24
|
expect(message).toContain('- failed commands');
|
|
25
25
|
expect(message).toContain('<callout accent="#f59e0b">');
|
|
26
26
|
});
|
|
27
|
+
test('requires interactive tools after all text, using exact tool names', () => {
|
|
28
|
+
const message = getOpencodeSystemMessage({
|
|
29
|
+
sessionId: 'ses_123',
|
|
30
|
+
});
|
|
31
|
+
expect(message).toContain('You MUST write ALL user-visible text FIRST');
|
|
32
|
+
expect(message).toContain('You MUST call `question` LAST');
|
|
33
|
+
expect(message).toContain('NEVER call `question` before your text');
|
|
34
|
+
expect(message).toContain('`kimaki_action_buttons`');
|
|
35
|
+
expect(message).toContain('`kimaki_file_upload`');
|
|
36
|
+
});
|
|
27
37
|
test('persists and reads session system prompt for command path', async () => {
|
|
28
38
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-system-'));
|
|
29
39
|
tempDirs.push(dataDir);
|
|
@@ -174,7 +184,8 @@ describe('system-message', () => {
|
|
|
174
184
|
|
|
175
185
|
## requesting files from the user
|
|
176
186
|
|
|
177
|
-
To ask the user to upload files from their device, use
|
|
187
|
+
To ask the user to upload files from their device, use \`kimaki_file_upload\`. This shows a native file picker dialog in Discord. The files are downloaded to the project's \`uploads/\` directory and the tool returns the local file paths.
|
|
188
|
+
You MUST call \`kimaki_file_upload\` LAST, after ALL text.
|
|
178
189
|
|
|
179
190
|
## archiving the current thread
|
|
180
191
|
|
|
@@ -295,6 +306,10 @@ describe('system-message', () => {
|
|
|
295
306
|
kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
|
|
296
307
|
kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
|
|
297
308
|
|
|
309
|
+
Use \`--pre-run '<command>'\` to check whether a scheduled task should start. Kimaki runs the command in the project directory. Exit code 0 starts the session and appends stdout to the prompt. Any other exit code skips that occurrence. Command output is written to the Kimaki log.
|
|
310
|
+
|
|
311
|
+
Scheduled tasks do not overlap by default. Add \`--allow-concurrency\` only when concurrent sessions from the same task are safe.
|
|
312
|
+
|
|
298
313
|
**ALWAYS pass \`--user\` when scheduling a task.** Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member, so the task can fire completely unnoticed if the user never joined the thread or already left it. This applies to \`--channel\` and \`--thread\` scheduling alike.
|
|
299
314
|
|
|
300
315
|
ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
|
|
@@ -304,6 +319,8 @@ describe('system-message', () => {
|
|
|
304
319
|
- \`--notify-only\` to create a reminder thread without auto-starting a session
|
|
305
320
|
- \`--worktree\` to create the scheduled thread as a worktree session (only if the user explicitly asks for a worktree)
|
|
306
321
|
- \`--agent\` and \`--model\` to control scheduled session behavior
|
|
322
|
+
- \`--pre-run\` to start only when a project command exits with code 0
|
|
323
|
+
- \`--allow-concurrency\` to permit overlapping runs from the same task
|
|
307
324
|
- \`--parent-session\` to pass this session as parent of the scheduled child
|
|
308
325
|
- \`--user\` to add a specific user to the scheduled thread (always pass this)
|
|
309
326
|
|
|
@@ -337,7 +354,7 @@ describe('system-message', () => {
|
|
|
337
354
|
Manage scheduled tasks with:
|
|
338
355
|
|
|
339
356
|
kimaki task list
|
|
340
|
-
kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
|
|
357
|
+
kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--pre-run "command"] [--allow-concurrency true|false] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
|
|
341
358
|
kimaki task delete <id>
|
|
342
359
|
|
|
343
360
|
\`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Fix it with \`kimaki task edit <id> --user '<discord-user-id>'\` instead of deleting and recreating the task. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
|
|
@@ -782,9 +799,16 @@ describe('system-message', () => {
|
|
|
782
799
|
|
|
783
800
|
## ending conversations with options
|
|
784
801
|
|
|
785
|
-
|
|
802
|
+
You MUST write ALL user-visible text FIRST.
|
|
803
|
+
You MUST call \`question\` LAST, after ALL text parts.
|
|
804
|
+
NEVER call \`question\` before your text. Discord will hide the message.
|
|
805
|
+
|
|
806
|
+
The same rule applies to \`kimaki_action_buttons\` and \`kimaki_file_upload\`.
|
|
807
|
+
You MUST call them LAST, after ALL text.
|
|
808
|
+
|
|
809
|
+
ALWAYS use \`question\` when you ask the user a question. Do not write a numbered list in plain text.
|
|
786
810
|
|
|
787
|
-
IMPORTANT: Do NOT use
|
|
811
|
+
IMPORTANT: Do NOT use \`question\` to ask permission before doing work. Do the work first, then offer follow-ups.
|
|
788
812
|
|
|
789
813
|
Examples:
|
|
790
814
|
- After completing edits: offer "Commit changes?"
|