@jpcasabiancai/cayenne 0.5.0 → 0.5.2
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/cli.js +356 -75
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -301,7 +301,8 @@ var init_repositories = __esm({
|
|
|
301
301
|
});
|
|
302
302
|
RepositoryList = z4.object({
|
|
303
303
|
repositories: z4.array(RepositoryListItem).max(2e3),
|
|
304
|
-
connected: z4.boolean()
|
|
304
|
+
connected: z4.boolean(),
|
|
305
|
+
reason: z4.literal("github_not_installed").optional()
|
|
305
306
|
});
|
|
306
307
|
REPOSITORY_LIST_ERRORS = [
|
|
307
308
|
"unauthorized",
|
|
@@ -325,7 +326,12 @@ var init_repositories = __esm({
|
|
|
325
326
|
fullName: z4.string().min(1)
|
|
326
327
|
});
|
|
327
328
|
RepoSessionTokenRequest = z4.strictObject({ sessionId: z4.uuid() });
|
|
328
|
-
REPO_READ_ONLY_REASONS = [
|
|
329
|
+
REPO_READ_ONLY_REASONS = [
|
|
330
|
+
"not_connected",
|
|
331
|
+
"no_expiry",
|
|
332
|
+
"scope_refused",
|
|
333
|
+
"github_not_installed"
|
|
334
|
+
];
|
|
329
335
|
RepoReadOnlyReason = z4.enum(REPO_READ_ONLY_REASONS);
|
|
330
336
|
RepoSessionToken = z4.discriminatedUnion("connected", [
|
|
331
337
|
z4.object({
|
|
@@ -354,6 +360,7 @@ var init_repositories = __esm({
|
|
|
354
360
|
"not_recent",
|
|
355
361
|
"not_found",
|
|
356
362
|
"not_installed",
|
|
363
|
+
"github_not_installed",
|
|
357
364
|
"archived",
|
|
358
365
|
"repository_mismatch",
|
|
359
366
|
"not_configured",
|
|
@@ -460,7 +467,7 @@ var init_skills = __esm({
|
|
|
460
467
|
|
|
461
468
|
// ../../packages/shared/src/protocol/integrations.ts
|
|
462
469
|
import { z as z6 } from "zod";
|
|
463
|
-
var INTEGRATION_PROVIDERS, AUTH_KINDS, both, PROVIDER_META, IntegrationProviderSchema, AuthKindSchema, ProviderConfigPublic, ConnectStartRequest, ConnectStartResponse, ConnectCompleteRequest, DisconnectRequest, MessageResourceKind, ReplyRequest, ReplyResponse, ResourceList, SetSubscriptionsRequest, INBOX_ERROR_CODES, InboxErrorCode, CONVERSATION_KINDS, ConversationKind;
|
|
470
|
+
var INTEGRATION_PROVIDERS, AUTH_KINDS, both, PROVIDER_META, IntegrationProviderSchema, AuthKindSchema, ProviderConfigPublic, ConnectStartRequest, ConnectStartResponse, ConnectCompleteRequest, DisconnectRequest, GithubInstallationId, GithubAccountLogin, GithubAccountType, GithubAvatarUrl, GithubInstallation, GithubAvailableInstallation, GithubInstallationList, GithubBindRequest, GithubBindResponse, GithubUnbindRequest, GithubUnbindResponse, MessageResourceKind, ReplyRequest, ReplyResponse, ResourceList, SetSubscriptionsRequest, INBOX_ERROR_CODES, InboxErrorCode, CONVERSATION_KINDS, ConversationKind;
|
|
464
471
|
var init_integrations = __esm({
|
|
465
472
|
"../../packages/shared/src/protocol/integrations.ts"() {
|
|
466
473
|
"use strict";
|
|
@@ -513,9 +520,10 @@ var init_integrations = __esm({
|
|
|
513
520
|
github: {
|
|
514
521
|
label: "GitHub",
|
|
515
522
|
badge: "GH",
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
523
|
+
// The Cayenne GitHub App (D57), a team workspace's platform path; Composio is Stage 3.
|
|
524
|
+
paths: { native: null, composio: { stage: 3, toolkit: "github" }, platform: { stage: 1 } },
|
|
525
|
+
capabilities: { messages: [], tasks: ["composio", "platform"] },
|
|
526
|
+
webhook: { fn: "github-webhook", params: [] }
|
|
519
527
|
},
|
|
520
528
|
monday: {
|
|
521
529
|
label: "Monday.com",
|
|
@@ -577,11 +585,49 @@ var init_integrations = __esm({
|
|
|
577
585
|
ConnectStartRequest = z6.strictObject({
|
|
578
586
|
workspaceId: z6.uuid(),
|
|
579
587
|
provider: IntegrationProviderSchema,
|
|
580
|
-
acknowledged: z6.boolean().optional()
|
|
588
|
+
acknowledged: z6.boolean().optional(),
|
|
589
|
+
intent: z6.enum(["connect", "install"]).optional()
|
|
590
|
+
}).superRefine((r, ctx) => {
|
|
591
|
+
if (r.intent === "install" && r.provider !== "github") {
|
|
592
|
+
ctx.addIssue({ code: "custom", path: ["intent"], message: "install is GitHub only" });
|
|
593
|
+
}
|
|
581
594
|
});
|
|
582
595
|
ConnectStartResponse = z6.object({ url: z6.url({ protocol: /^https$/ }) });
|
|
583
596
|
ConnectCompleteRequest = z6.strictObject({ pendingId: z6.uuid() });
|
|
584
597
|
DisconnectRequest = z6.strictObject({ integrationId: z6.uuid() });
|
|
598
|
+
GithubInstallationId = z6.number().int().positive();
|
|
599
|
+
GithubAccountLogin = z6.string().min(1).max(100);
|
|
600
|
+
GithubAccountType = z6.enum(["Organization", "User"]);
|
|
601
|
+
GithubAvatarUrl = z6.url({
|
|
602
|
+
protocol: /^https$/,
|
|
603
|
+
hostname: /^avatars[.]githubusercontent[.]com$/
|
|
604
|
+
});
|
|
605
|
+
GithubInstallation = z6.object({
|
|
606
|
+
id: z6.uuid(),
|
|
607
|
+
installationId: GithubInstallationId,
|
|
608
|
+
accountLogin: GithubAccountLogin,
|
|
609
|
+
accountType: GithubAccountType,
|
|
610
|
+
avatarUrl: GithubAvatarUrl,
|
|
611
|
+
addedBy: z6.object({ id: z6.uuid(), displayName: z6.string().nullable() }).nullable(),
|
|
612
|
+
createdAt: z6.string()
|
|
613
|
+
});
|
|
614
|
+
GithubAvailableInstallation = z6.object({
|
|
615
|
+
installationId: GithubInstallationId,
|
|
616
|
+
accountLogin: GithubAccountLogin,
|
|
617
|
+
accountType: GithubAccountType,
|
|
618
|
+
avatarUrl: GithubAvatarUrl
|
|
619
|
+
});
|
|
620
|
+
GithubInstallationList = z6.object({
|
|
621
|
+
bound: z6.array(GithubInstallation),
|
|
622
|
+
available: z6.array(GithubAvailableInstallation)
|
|
623
|
+
});
|
|
624
|
+
GithubBindRequest = z6.strictObject({
|
|
625
|
+
workspaceId: z6.uuid(),
|
|
626
|
+
installationId: GithubInstallationId
|
|
627
|
+
});
|
|
628
|
+
GithubBindResponse = z6.object({ binding: GithubInstallation });
|
|
629
|
+
GithubUnbindRequest = z6.strictObject({ workspaceId: z6.uuid(), bindingId: z6.uuid() });
|
|
630
|
+
GithubUnbindResponse = z6.object({ orphaned: z6.number().int().nonnegative() });
|
|
585
631
|
MessageResourceKind = z6.enum(["clickup_team", "teams_channel", "teams_chat"]);
|
|
586
632
|
ReplyRequest = z6.strictObject({
|
|
587
633
|
messageId: z6.uuid(),
|
|
@@ -1507,10 +1553,10 @@ function assertClaudeDirExists(dir) {
|
|
|
1507
1553
|
}
|
|
1508
1554
|
}
|
|
1509
1555
|
function claudeEnv(dir) {
|
|
1510
|
-
return
|
|
1556
|
+
return { CLAUDE_CONFIG_DIR: dir };
|
|
1511
1557
|
}
|
|
1512
1558
|
function sessionClaudeConfigDir(sessionEnv, daemonEnv = process.env, home = homedir()) {
|
|
1513
|
-
return sessionEnv
|
|
1559
|
+
return { ...daemonEnv, ...sessionEnv }.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
|
|
1514
1560
|
}
|
|
1515
1561
|
function cliHash(text) {
|
|
1516
1562
|
let hash = 0;
|
|
@@ -2126,27 +2172,45 @@ async function updateRunner(db, runnerId, columns) {
|
|
|
2126
2172
|
}
|
|
2127
2173
|
async function stamp(db, runnerId, status, what) {
|
|
2128
2174
|
const { data, error } = await db.from("runners").update({ status, last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", runnerId).select("id");
|
|
2129
|
-
if (error)
|
|
2130
|
-
|
|
2175
|
+
if (error) {
|
|
2176
|
+
logger.warn({ runnerId, err: error.message }, `${what} write failed`);
|
|
2177
|
+
return "error";
|
|
2178
|
+
}
|
|
2179
|
+
return data && data.length > 0 ? "ok" : "missing";
|
|
2131
2180
|
}
|
|
2132
2181
|
function startHeartbeat(db, runnerId, intervalMs, refresh, published, current) {
|
|
2133
2182
|
const seed = published ? RunnerCapabilities.safeParse(published) : null;
|
|
2134
2183
|
let last = seed?.success ? JSON.stringify(seed.data) : null;
|
|
2135
2184
|
let stopped = false;
|
|
2136
2185
|
let writes = Promise.resolve();
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2186
|
+
let misses = 0;
|
|
2187
|
+
const tick = async () => {
|
|
2188
|
+
let outcome;
|
|
2189
|
+
try {
|
|
2190
|
+
outcome = await stamp(db, runnerId, "online", "heartbeat");
|
|
2191
|
+
} catch (err) {
|
|
2192
|
+
logger.warn({ runnerId, err }, "heartbeat write failed");
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
if (outcome === "missing") {
|
|
2196
|
+
if (misses === 0) logger.warn({ runnerId }, "heartbeat matched no runner row");
|
|
2197
|
+
misses += 1;
|
|
2198
|
+
} else if (outcome === "ok" && misses > 0) {
|
|
2199
|
+
logger.info({ runnerId, misses }, "heartbeat matched its runner row again");
|
|
2200
|
+
misses = 0;
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2140
2203
|
const timer = setInterval(() => void tick(), intervalMs);
|
|
2141
2204
|
const write = async (caps) => {
|
|
2142
2205
|
const json = JSON.stringify(caps);
|
|
2143
2206
|
if (stopped || json === last) return;
|
|
2144
2207
|
try {
|
|
2145
|
-
const { error } = await db.from("runners").update({ capabilities: caps }).eq("id", runnerId);
|
|
2208
|
+
const { data, error } = await db.from("runners").update({ capabilities: caps }).eq("id", runnerId).select("id");
|
|
2146
2209
|
if (error) {
|
|
2147
2210
|
logger.warn({ runnerId, err: error.message }, "capability refresh failed");
|
|
2148
2211
|
return;
|
|
2149
2212
|
}
|
|
2213
|
+
if (!data || data.length === 0) return;
|
|
2150
2214
|
last = json;
|
|
2151
2215
|
} catch (err) {
|
|
2152
2216
|
logger.warn({ runnerId, err }, "capability refresh failed");
|
|
@@ -2182,7 +2246,9 @@ function startHeartbeat(db, runnerId, intervalMs, refresh, published, current) {
|
|
|
2182
2246
|
};
|
|
2183
2247
|
}
|
|
2184
2248
|
async function markOffline(db, runnerId) {
|
|
2185
|
-
await stamp(db, runnerId, "offline", "markOffline")
|
|
2249
|
+
if (await stamp(db, runnerId, "offline", "markOffline") === "missing") {
|
|
2250
|
+
logger.warn({ runnerId }, "markOffline matched no runner row");
|
|
2251
|
+
}
|
|
2186
2252
|
}
|
|
2187
2253
|
var UNCLAIMED_PROFILE, IDENTITY_ATTEMPTS, IDENTITY_RETRY_MS, wait, CAPABILITIES_REFRESH_MS;
|
|
2188
2254
|
var init_registry2 = __esm({
|
|
@@ -2811,7 +2877,7 @@ var init_dispatcher = __esm({
|
|
|
2811
2877
|
|
|
2812
2878
|
// src/commands/source.ts
|
|
2813
2879
|
import { LRUCache } from "lru-cache";
|
|
2814
|
-
var STOPPING, REJOIN_MIN_MS, REJOIN_MAX_MS, SOCKET_RESET_MS, DRAIN_WHILE_DOWN_MS, DRAIN_READ_TIMEOUT_MS, LOST, messageOf2, byCreatedAt, GIT_READ_KINDS, GIT_WORKTREE_KINDS, laneOf, CommandSource;
|
|
2880
|
+
var STOPPING, REJOIN_MIN_MS, REJOIN_MAX_MS, SOCKET_RESET_MS, DRAIN_WHILE_DOWN_MS, DRAIN_READ_TIMEOUT_MS, DORMANT_PROBE_MS, LOST, messageOf2, byCreatedAt, GIT_READ_KINDS, GIT_WORKTREE_KINDS, laneOf, CommandSource;
|
|
2815
2881
|
var init_source = __esm({
|
|
2816
2882
|
"src/commands/source.ts"() {
|
|
2817
2883
|
"use strict";
|
|
@@ -2823,6 +2889,7 @@ var init_source = __esm({
|
|
|
2823
2889
|
SOCKET_RESET_MS = 6e4;
|
|
2824
2890
|
DRAIN_WHILE_DOWN_MS = 5e3;
|
|
2825
2891
|
DRAIN_READ_TIMEOUT_MS = 5e3;
|
|
2892
|
+
DORMANT_PROBE_MS = 6e4;
|
|
2826
2893
|
LOST = /* @__PURE__ */ new Set(["CHANNEL_ERROR", "TIMED_OUT", "CLOSED"]);
|
|
2827
2894
|
messageOf2 = (err) => err instanceof Error ? err.message : String(err);
|
|
2828
2895
|
byCreatedAt = (a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) || a.id.localeCompare(b.id);
|
|
@@ -2850,6 +2917,8 @@ var init_source = __esm({
|
|
|
2850
2917
|
this.now = opts.now ?? Date.now;
|
|
2851
2918
|
this.secrets = opts.secrets ?? secrets;
|
|
2852
2919
|
this.timings = opts.timings;
|
|
2920
|
+
this.owner = opts.owner;
|
|
2921
|
+
this.onOwnRowVisibility = opts.onOwnRowVisibility;
|
|
2853
2922
|
}
|
|
2854
2923
|
db;
|
|
2855
2924
|
runnerId;
|
|
@@ -2887,11 +2956,18 @@ var init_source = __esm({
|
|
|
2887
2956
|
downDrain = null;
|
|
2888
2957
|
/** Whether the live channel's last status was `SUBSCRIBED`. */
|
|
2889
2958
|
subscribed = false;
|
|
2959
|
+
/**
|
|
2960
|
+
* The last probe of this runner's own row found it invisible (S-42, 0039): no joins, no drain
|
|
2961
|
+
* timer, a probe every `DORMANT_PROBE_MS`. Set with one error line, cleared with one info line.
|
|
2962
|
+
*/
|
|
2963
|
+
dormant = false;
|
|
2890
2964
|
/** Drain reads that failed in a row; only the first of a streak is logged as an error. */
|
|
2891
2965
|
drainFailures = 0;
|
|
2892
2966
|
/** Broadcasts and pointers held back while a drain is in flight; see `drain()` for why. */
|
|
2893
2967
|
pending = [];
|
|
2894
2968
|
timings;
|
|
2969
|
+
owner;
|
|
2970
|
+
onOwnRowVisibility;
|
|
2895
2971
|
/** Join `runner:{id}` at once; no connectedness check, since `subscribe` connects the socket. */
|
|
2896
2972
|
async start() {
|
|
2897
2973
|
this.drainWhileDown();
|
|
@@ -2963,9 +3039,15 @@ var init_source = __esm({
|
|
|
2963
3039
|
this.attempts = 0;
|
|
2964
3040
|
this.socketDownSince = null;
|
|
2965
3041
|
this.subscribed = true;
|
|
3042
|
+
if (this.dormant) this.wake();
|
|
2966
3043
|
void this.drain();
|
|
2967
3044
|
} else if (LOST.has(status)) {
|
|
2968
3045
|
this.subscribed = false;
|
|
3046
|
+
if (this.dormant) {
|
|
3047
|
+
this.abandon(ch);
|
|
3048
|
+
this.goDormant();
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
2969
3051
|
this.drainWhileDown();
|
|
2970
3052
|
this.abandon(ch);
|
|
2971
3053
|
this.scheduleRejoin(status);
|
|
@@ -3012,6 +3094,16 @@ var init_source = __esm({
|
|
|
3012
3094
|
* instance is created. A socket that is down gets a `connect()` and no join: it buffers every
|
|
3013
3095
|
* push while down and flushes them all on reopen, so a join per attempt through a long outage
|
|
3014
3096
|
* would arrive as a burst of joins and leaves.
|
|
3097
|
+
*
|
|
3098
|
+
* On a connected socket, this runner's own row is read before the join (BACKLOG S-42, 0039). A
|
|
3099
|
+
* channel lost while the socket is up is either transient or a refusal: once the owner is
|
|
3100
|
+
* removed from the runner's workspace, Realtime closes `runner:{id}` and answers every rejoin
|
|
3101
|
+
* `CHANNEL_ERROR`, the same status an outage reports. The row tells the two apart without
|
|
3102
|
+
* matching Realtime's error text, for one REST read per attempt: `runners_select` is
|
|
3103
|
+
* `is_member(workspace_id)` and a runner never lives in a personal space (0034), so the row is
|
|
3104
|
+
* visible to the owner exactly while `owns_runner` holds. Invisible, or a row that no longer names
|
|
3105
|
+
* this owner in its workspace (`owner`) → dormant (see `goDormant`). A read that fails says nothing
|
|
3106
|
+
* either way, and never blocks recovery: the attempt joins.
|
|
3015
3107
|
*/
|
|
3016
3108
|
async rejoin() {
|
|
3017
3109
|
await this.removal;
|
|
@@ -3023,6 +3115,13 @@ var init_source = __esm({
|
|
|
3023
3115
|
return;
|
|
3024
3116
|
}
|
|
3025
3117
|
this.socketDownSince = null;
|
|
3118
|
+
const visible = await this.ownRowVisible();
|
|
3119
|
+
if (this.stopped) return;
|
|
3120
|
+
if (visible === false) {
|
|
3121
|
+
this.goDormant();
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (visible && this.dormant) this.wake();
|
|
3026
3125
|
const ch = this.runnerChannel();
|
|
3027
3126
|
if (this.abandoned.has(ch)) {
|
|
3028
3127
|
this.log.warn({ attempt: this.attempts }, "runner channel still held after leaving it");
|
|
@@ -3040,6 +3139,63 @@ var init_source = __esm({
|
|
|
3040
3139
|
this.scheduleRejoin("JOIN_FAILED");
|
|
3041
3140
|
}
|
|
3042
3141
|
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Whether the owner this daemon logs in as can see this runner's row, still naming them in the
|
|
3144
|
+
* workspace it registered in: `true`, `false`, or `null` when the read failed, threw or timed out
|
|
3145
|
+
* (bounded like a drain read). Never rejects.
|
|
3146
|
+
*/
|
|
3147
|
+
async ownRowVisible() {
|
|
3148
|
+
try {
|
|
3149
|
+
const { data, error } = await this.db.from("runners").select("id, user_id, workspace_id").eq("id", this.runnerId).abortSignal(AbortSignal.timeout(DRAIN_READ_TIMEOUT_MS)).maybeSingle();
|
|
3150
|
+
if (!error) {
|
|
3151
|
+
if (data === null) return false;
|
|
3152
|
+
const { owner } = this;
|
|
3153
|
+
return !owner || data.user_id === owner.userId && data.workspace_id === owner.workspaceId;
|
|
3154
|
+
}
|
|
3155
|
+
this.log.warn({ err: error.message }, "could not read this runner's own row; rejoining");
|
|
3156
|
+
} catch (err) {
|
|
3157
|
+
this.log.warn({ err: messageOf2(err) }, "could not read this runner's own row; rejoining");
|
|
3158
|
+
}
|
|
3159
|
+
return null;
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Stop asking for commands nobody may send (S-42, 0039): no join (Realtime would refuse it), no
|
|
3163
|
+
* drain timer (RLS hides every row), and one error line on entering, not one per probe. The next
|
|
3164
|
+
* attempt is a probe `DORMANT_PROBE_MS` away, in the rejoin slot so `stop()` clears it, and it
|
|
3165
|
+
* does not move the backoff.
|
|
3166
|
+
*/
|
|
3167
|
+
goDormant() {
|
|
3168
|
+
this.stopDrainWhileDown();
|
|
3169
|
+
if (!this.dormant) {
|
|
3170
|
+
this.dormant = true;
|
|
3171
|
+
this.log.error(
|
|
3172
|
+
{ runnerId: this.runnerId },
|
|
3173
|
+
"this runner's own row is not visible to its owner, or no longer names this owner in its workspace: the owner left the runner's workspace, or the row was deleted or replaced; commands are paused until it is visible again"
|
|
3174
|
+
);
|
|
3175
|
+
this.tell(false);
|
|
3176
|
+
}
|
|
3177
|
+
if (this.rejoinTimer) return;
|
|
3178
|
+
this.rejoinTimer = setTimeout(() => {
|
|
3179
|
+
this.rejoinTimer = null;
|
|
3180
|
+
void this.rejoin();
|
|
3181
|
+
}, DORMANT_PROBE_MS);
|
|
3182
|
+
}
|
|
3183
|
+
/** The row is visible again: rejoin from the backoff's first step, draining meanwhile. */
|
|
3184
|
+
wake() {
|
|
3185
|
+
this.dormant = false;
|
|
3186
|
+
this.attempts = 0;
|
|
3187
|
+
this.log.info({ runnerId: this.runnerId }, "runner row visible again; rejoining");
|
|
3188
|
+
this.drainWhileDown();
|
|
3189
|
+
this.tell(true);
|
|
3190
|
+
}
|
|
3191
|
+
/** `onOwnRowVisibility`, kept from ever breaking the source. */
|
|
3192
|
+
tell(visible) {
|
|
3193
|
+
try {
|
|
3194
|
+
this.onOwnRowVisibility?.(visible);
|
|
3195
|
+
} catch (err) {
|
|
3196
|
+
this.log.warn({ visible, err: messageOf2(err) }, "own-row visibility listener threw");
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3043
3199
|
/**
|
|
3044
3200
|
* Ask a down socket to connect. Once attempts have found it down for `SOCKET_RESET_MS`, tear it
|
|
3045
3201
|
* down first: `connect()` returns early while the client still holds a conn, and a conn whose
|
|
@@ -3774,13 +3930,14 @@ function writeShim(path, body) {
|
|
|
3774
3930
|
function shellQuote(value) {
|
|
3775
3931
|
return `'${value.replaceAll("'", String.raw`'\''`)}'`;
|
|
3776
3932
|
}
|
|
3777
|
-
var READ_ONLY_NOT_CONNECTED, READ_ONLY_NO_EXPIRY, READ_ONLY_SCOPE_REFUSED, READ_ONLY_UNAVAILABLE, CREDENTIAL_HELPER, NO_SSH_TEXT, READ_ONLY_IDENTITY, GH_SHIM, NO_SSH_SHIM, GIT_NOT_FOUND_TEXT, GH_CONFIG, READONLY_BIN, NO_SSH, GIT_HOME, GIT_BIN, NETRC_NAME, CURLRC_NAME, NETRC_VALUE_KEYS;
|
|
3933
|
+
var READ_ONLY_NOT_CONNECTED, READ_ONLY_NO_EXPIRY, READ_ONLY_GITHUB_NOT_INSTALLED, READ_ONLY_SCOPE_REFUSED, READ_ONLY_UNAVAILABLE, CREDENTIAL_HELPER, NO_SSH_TEXT, READ_ONLY_IDENTITY, GH_SHIM, NO_SSH_SHIM, GIT_NOT_FOUND_TEXT, GH_CONFIG, READONLY_BIN, NO_SSH, GIT_HOME, GIT_BIN, NETRC_NAME, CURLRC_NAME, NETRC_VALUE_KEYS;
|
|
3778
3934
|
var init_credentials = __esm({
|
|
3779
3935
|
"src/repos/credentials.ts"() {
|
|
3780
3936
|
"use strict";
|
|
3781
3937
|
init_introspect();
|
|
3782
3938
|
READ_ONLY_NOT_CONNECTED = "Cayenne: this repository chat is read-only because you have not connected GitHub. Connect it in Settings \u2192 Integrations, then send your next message here.";
|
|
3783
|
-
READ_ONLY_NO_EXPIRY =
|
|
3939
|
+
READ_ONLY_NO_EXPIRY = "Cayenne: this repository chat is read-only on this runner because your GitHub token never expires, and Cayenne lends only expiring tokens to another member's runner. Reconnect GitHub in Settings \u2192 Integrations to get a token that expires, then send your next message here.";
|
|
3940
|
+
READ_ONLY_GITHUB_NOT_INSTALLED = "Cayenne: this repository chat is read-only because this repository's GitHub organisation is not connected to this workspace. Add it in Settings \u2192 Integrations, then send your next message here.";
|
|
3784
3941
|
READ_ONLY_SCOPE_REFUSED = "Cayenne: this repository chat is read-only because GitHub would not give your token access to this repository; your GitHub account may not have access to it. Send another message here once it does.";
|
|
3785
3942
|
READ_ONLY_UNAVAILABLE = "Cayenne: this repository chat could not get your GitHub token, so pushing is off for now. Send another message to retry.";
|
|
3786
3943
|
CREDENTIAL_HELPER = String.raw`!f() { test "$1" = get || exit 0; if [ -n "$GH_TOKEN" ]; then echo username=x-access-token; echo "password=$GH_TOKEN"; else printf '%s\n' "$CAYENNE_READ_ONLY" >&2; exit 1; fi; }; f`;
|
|
@@ -6093,13 +6250,16 @@ async function failureCode(error, signal) {
|
|
|
6093
6250
|
function cloneRefusal(code, slug) {
|
|
6094
6251
|
switch (code) {
|
|
6095
6252
|
case "not_installed":
|
|
6096
|
-
return `VALIDATION: ${slug} is not in the
|
|
6253
|
+
return `VALIDATION: ${slug} is not in the Cayenne GitHub App's installation`;
|
|
6254
|
+
// D57: the repository's installation is not bound to this workspace, or there is none.
|
|
6255
|
+
case "github_not_installed":
|
|
6256
|
+
return `VALIDATION: ${slug}'s GitHub organisation is not connected to this workspace; add it in Settings \u2192 Integrations`;
|
|
6097
6257
|
case "repository_mismatch":
|
|
6098
6258
|
return `VALIDATION: ${slug} was renamed or replaced on GitHub; pick it again`;
|
|
6099
6259
|
case "bad_default_branch":
|
|
6100
6260
|
return `VALIDATION: ${slug}'s default branch is not a branch name Cayenne accepts`;
|
|
6101
6261
|
case "permissions_pending":
|
|
6102
|
-
return "PROVIDER_ERROR: the
|
|
6262
|
+
return "PROVIDER_ERROR: the Cayenne GitHub App needs Contents access; an admin must approve the new permissions on GitHub";
|
|
6103
6263
|
// Also a private key GitHub refuses or a deleted App: only an admin can fix either.
|
|
6104
6264
|
case "not_configured":
|
|
6105
6265
|
return "PROVIDER_ERROR: GitHub is not set up correctly for this workspace; ask an admin";
|
|
@@ -6211,10 +6371,12 @@ var init_roster = __esm({
|
|
|
6211
6371
|
});
|
|
6212
6372
|
return this.inflight;
|
|
6213
6373
|
}
|
|
6214
|
-
/**
|
|
6374
|
+
/**
|
|
6375
|
+
* The runner owner, or a listed member who is still a workspace member — and nobody at all
|
|
6376
|
+
* while the owner is not a member of the runner's workspace (0039).
|
|
6377
|
+
*/
|
|
6215
6378
|
serves(userId) {
|
|
6216
|
-
|
|
6217
|
-
return this.allowedMemberIds.includes(userId);
|
|
6379
|
+
return this.ownerIsMember && (userId === this.deps.runnerUserId || this.allowedMemberIds.includes(userId));
|
|
6218
6380
|
}
|
|
6219
6381
|
/**
|
|
6220
6382
|
* False once `ROSTER_FAIL_CLOSED_AFTER` refresh intervals have passed with no successful read,
|
|
@@ -6240,16 +6402,20 @@ var init_roster = __esm({
|
|
|
6240
6402
|
if (channel) await this.deps.db.removeChannel(channel);
|
|
6241
6403
|
}
|
|
6242
6404
|
async load() {
|
|
6243
|
-
const { db, runnerId, workspaceId, runnerUserId
|
|
6405
|
+
const { db, runnerId, workspaceId, runnerUserId } = this.deps;
|
|
6244
6406
|
const runner = await this.withTimeout(
|
|
6245
|
-
db.from("runners").select("allowed_member_ids").eq("id", runnerId).
|
|
6407
|
+
db.from("runners").select("allowed_member_ids, user_id, workspace_id").eq("id", runnerId).maybeSingle(),
|
|
6246
6408
|
`could not read allowed_member_ids for runner ${runnerId}: timed out`
|
|
6247
6409
|
);
|
|
6248
|
-
if (runner.error
|
|
6410
|
+
if (runner.error) {
|
|
6249
6411
|
throw new Error(
|
|
6250
|
-
`could not read allowed_member_ids for runner ${runnerId}: ${runner.error
|
|
6412
|
+
`could not read allowed_member_ids for runner ${runnerId}: ${runner.error.message}`
|
|
6251
6413
|
);
|
|
6252
6414
|
}
|
|
6415
|
+
if (!runner.data || runner.data.user_id !== runnerUserId || runner.data.workspace_id !== workspaceId) {
|
|
6416
|
+
this.apply([], false, []);
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6253
6419
|
const listed = runner.data.allowed_member_ids;
|
|
6254
6420
|
const members = await this.withTimeout(
|
|
6255
6421
|
db.from("workspace_members").select("user_id").eq("workspace_id", workspaceId).in("user_id", [runnerUserId, ...listed]),
|
|
@@ -6261,15 +6427,18 @@ var init_roster = __esm({
|
|
|
6261
6427
|
);
|
|
6262
6428
|
}
|
|
6263
6429
|
const current = new Set(members.data.map((m) => m.user_id));
|
|
6430
|
+
const ownerIsMember = current.has(runnerUserId);
|
|
6431
|
+
const served = ownerIsMember ? listed.filter((id) => id !== runnerUserId && current.has(id)) : [];
|
|
6432
|
+
this.apply(listed, ownerIsMember, served);
|
|
6433
|
+
}
|
|
6434
|
+
/** Record one successful read: the lists spliced in place, the time, and a line on a change. */
|
|
6435
|
+
apply(listed, ownerIsMember, served) {
|
|
6436
|
+
const { runnerId, log } = this.deps;
|
|
6264
6437
|
const before = [...this.allowedMemberIds];
|
|
6265
6438
|
const beforeOwnerIsMember = this.ownerIsMember;
|
|
6266
6439
|
this.listed = [...listed];
|
|
6267
|
-
this.ownerIsMember =
|
|
6268
|
-
this.allowedMemberIds.splice(
|
|
6269
|
-
0,
|
|
6270
|
-
this.allowedMemberIds.length,
|
|
6271
|
-
...listed.filter((id) => id !== runnerUserId && current.has(id))
|
|
6272
|
-
);
|
|
6440
|
+
this.ownerIsMember = ownerIsMember;
|
|
6441
|
+
this.allowedMemberIds.splice(0, this.allowedMemberIds.length, ...served);
|
|
6273
6442
|
this.lastSuccessAt = performance.now();
|
|
6274
6443
|
if (!sameIds(before, this.allowedMemberIds) || beforeOwnerIsMember !== this.ownerIsMember) {
|
|
6275
6444
|
log.info(
|
|
@@ -6847,6 +7016,31 @@ var init_handlers = __esm({
|
|
|
6847
7016
|
}
|
|
6848
7017
|
});
|
|
6849
7018
|
|
|
7019
|
+
// src/broadcast.ts
|
|
7020
|
+
import { REALTIME_CHANNEL_STATES } from "@supabase/supabase-js";
|
|
7021
|
+
function sendOnChannel(channel) {
|
|
7022
|
+
return (message, timeoutMs) => canPush(channel) ? channel.send(message, { timeout: timeoutMs }) : sendOverRest(channel, message, timeoutMs);
|
|
7023
|
+
}
|
|
7024
|
+
async function sendOverRest(channel, message, timeoutMs) {
|
|
7025
|
+
let sent;
|
|
7026
|
+
try {
|
|
7027
|
+
sent = await channel.httpSend(message.event, message.payload, { timeout: timeoutMs });
|
|
7028
|
+
} catch (err) {
|
|
7029
|
+
if (isAbort(err)) return "timed out";
|
|
7030
|
+
throw err;
|
|
7031
|
+
}
|
|
7032
|
+
if (sent.success) return "ok";
|
|
7033
|
+
throw new Error(`httpSend ${sent.status}: ${sent.error}`);
|
|
7034
|
+
}
|
|
7035
|
+
var isAbort, canPush;
|
|
7036
|
+
var init_broadcast = __esm({
|
|
7037
|
+
"src/broadcast.ts"() {
|
|
7038
|
+
"use strict";
|
|
7039
|
+
isAbort = (err) => err?.name === "AbortError";
|
|
7040
|
+
canPush = (channel) => channel.state === REALTIME_CHANNEL_STATES.joined && channel.socket.isConnected();
|
|
7041
|
+
}
|
|
7042
|
+
});
|
|
7043
|
+
|
|
6850
7044
|
// src/documents/paths.ts
|
|
6851
7045
|
import { realpathSync as realpathSync11 } from "fs";
|
|
6852
7046
|
import { isAbsolute as isAbsolute12, join as join20, relative as relative6, sep as sep9 } from "path";
|
|
@@ -7386,6 +7580,18 @@ var init_service = __esm({
|
|
|
7386
7580
|
}
|
|
7387
7581
|
});
|
|
7388
7582
|
|
|
7583
|
+
// src/providers/env.ts
|
|
7584
|
+
function definedEnv(env) {
|
|
7585
|
+
const out = {};
|
|
7586
|
+
for (const [key, value] of Object.entries(env)) if (value !== void 0) out[key] = value;
|
|
7587
|
+
return out;
|
|
7588
|
+
}
|
|
7589
|
+
var init_env = __esm({
|
|
7590
|
+
"src/providers/env.ts"() {
|
|
7591
|
+
"use strict";
|
|
7592
|
+
}
|
|
7593
|
+
});
|
|
7594
|
+
|
|
7389
7595
|
// src/claude/auth-mode.ts
|
|
7390
7596
|
function authModeFrom(info) {
|
|
7391
7597
|
if (info.apiProvider && info.apiProvider !== "firstParty") return "third_party";
|
|
@@ -7547,17 +7753,13 @@ async function askGuard(guard, toolName2, input) {
|
|
|
7547
7753
|
return GUARD_FAILED;
|
|
7548
7754
|
}
|
|
7549
7755
|
}
|
|
7550
|
-
function definedEnv(env) {
|
|
7551
|
-
const out = {};
|
|
7552
|
-
for (const [key, value] of Object.entries(env)) if (value !== void 0) out[key] = value;
|
|
7553
|
-
return out;
|
|
7554
|
-
}
|
|
7555
7756
|
var SESSION_FLAG_SETTINGS, GUARD_FAILED, ASK_TOOL_REASON, ClaudeSession;
|
|
7556
7757
|
var init_session2 = __esm({
|
|
7557
7758
|
"src/claude/session.ts"() {
|
|
7558
7759
|
"use strict";
|
|
7559
7760
|
init_src();
|
|
7560
7761
|
init_log();
|
|
7762
|
+
init_env();
|
|
7561
7763
|
init_auth_mode();
|
|
7562
7764
|
init_normalize();
|
|
7563
7765
|
init_pushable();
|
|
@@ -7945,6 +8147,7 @@ var init_session3 = __esm({
|
|
|
7945
8147
|
"src/codex/session.ts"() {
|
|
7946
8148
|
"use strict";
|
|
7947
8149
|
init_src();
|
|
8150
|
+
init_env();
|
|
7948
8151
|
init_attachments2();
|
|
7949
8152
|
init_normalize2();
|
|
7950
8153
|
init_policy();
|
|
@@ -8002,8 +8205,12 @@ var init_session3 = __esm({
|
|
|
8002
8205
|
questionCounts = /* @__PURE__ */ new Map();
|
|
8003
8206
|
start() {
|
|
8004
8207
|
if (this.child) throw new Error("VALIDATION: session already started");
|
|
8005
|
-
const spawnFn = this.opts.spawnFn ?? ((p, a) => spawn2(p, a, { stdio: "pipe" }));
|
|
8006
|
-
const child = spawnFn(
|
|
8208
|
+
const spawnFn = this.opts.spawnFn ?? ((p, a, env) => spawn2(p, a, { stdio: "pipe", env }));
|
|
8209
|
+
const child = spawnFn(
|
|
8210
|
+
this.opts.codexPath,
|
|
8211
|
+
["app-server"],
|
|
8212
|
+
definedEnv({ ...process.env, ...this.opts.env })
|
|
8213
|
+
);
|
|
8007
8214
|
this.child = child;
|
|
8008
8215
|
child.stderr?.setEncoding("utf8");
|
|
8009
8216
|
child.stderr?.on("data", (line) => {
|
|
@@ -9760,6 +9967,7 @@ var init_manager = __esm({
|
|
|
9760
9967
|
"src/sessions/manager.ts"() {
|
|
9761
9968
|
"use strict";
|
|
9762
9969
|
init_src();
|
|
9970
|
+
init_broadcast();
|
|
9763
9971
|
init_claude_dir();
|
|
9764
9972
|
init_config();
|
|
9765
9973
|
init_paths2();
|
|
@@ -9786,7 +9994,8 @@ var init_manager = __esm({
|
|
|
9786
9994
|
READ_ONLY_TEXT = {
|
|
9787
9995
|
not_connected: READ_ONLY_NOT_CONNECTED,
|
|
9788
9996
|
no_expiry: READ_ONLY_NO_EXPIRY,
|
|
9789
|
-
scope_refused: READ_ONLY_SCOPE_REFUSED
|
|
9997
|
+
scope_refused: READ_ONLY_SCOPE_REFUSED,
|
|
9998
|
+
github_not_installed: READ_ONLY_GITHUB_NOT_INSTALLED
|
|
9790
9999
|
};
|
|
9791
10000
|
REFRESH_WITHIN_MS = 10 * 6e4;
|
|
9792
10001
|
betweenTurns = (live) => (live.status === "idle" || live.status === "error") && !live.stopping;
|
|
@@ -10460,8 +10669,9 @@ var init_manager = __esm({
|
|
|
10460
10669
|
/**
|
|
10461
10670
|
* The runner's own re-check of who queued a `session.*` command, after the RPC's.
|
|
10462
10671
|
*
|
|
10463
|
-
* `created_by` must be a member this runner serves
|
|
10464
|
-
* `runners.allowed_member_ids`
|
|
10672
|
+
* `created_by` must be a member this runner serves, as the live roster decides (`serves`): the
|
|
10673
|
+
* runner owner or one of `runners.allowed_member_ids` still in the workspace, and nobody while
|
|
10674
|
+
* the owner is not a member of the runner's workspace (0039). The session owner gets no separate
|
|
10465
10675
|
* branch: `sessions_insert` already required `can_command_runner`, so every owner was on the
|
|
10466
10676
|
* list when the session was created, and an owner off it now has been de-listed since. A
|
|
10467
10677
|
* command with no creator is a flow's (data model §2), which phase 1A does not run.
|
|
@@ -10496,11 +10706,12 @@ var init_manager = __esm({
|
|
|
10496
10706
|
/**
|
|
10497
10707
|
* Whether this creator may command this machine at all.
|
|
10498
10708
|
*
|
|
10499
|
-
* The same membership test `assertMayCommand` runs (the runner owner
|
|
10500
|
-
*
|
|
10501
|
-
* session-scoped: `github.request` addresses a
|
|
10502
|
-
* reject it outright. One roster, one decision
|
|
10503
|
-
* only differ in the noun they name in the refusal,
|
|
10709
|
+
* The same membership test `assertMayCommand` runs (the roster's `serves`: the runner owner or
|
|
10710
|
+
* a listed member, and nobody while the owner is out of the runner's workspace, 0039), given
|
|
10711
|
+
* its own name because not every command is session-scoped: `github.request` addresses a
|
|
10712
|
+
* repository, and `requireSession` would reject it outright. One roster, one decision
|
|
10713
|
+
* (`serves`); this and `assertMayCommand` only differ in the noun they name in the refusal,
|
|
10714
|
+
* never in who they let through.
|
|
10504
10715
|
*/
|
|
10505
10716
|
assertMayUseRunner(createdBy) {
|
|
10506
10717
|
if (createdBy === null || !this.serves(createdBy)) {
|
|
@@ -10763,7 +10974,9 @@ var init_manager = __esm({
|
|
|
10763
10974
|
}
|
|
10764
10975
|
/**
|
|
10765
10976
|
* Put rows on this runner that claim a turn in progress back to `idle`, for the chats that have
|
|
10766
|
-
* no live session here — at boot every one of them, since nothing is live yet
|
|
10977
|
+
* no live session here — at boot every one of them, since nothing is live yet, and again when
|
|
10978
|
+
* the runner owner is back in the runner's workspace (S-42, `cayenne start`), minus the sessions
|
|
10979
|
+
* live by then. Only
|
|
10767
10980
|
* `running` and `awaiting_permission`: those are the states a dead provider cannot leave, while
|
|
10768
10981
|
* `starting` is still owed a `session.start` and `error` is the member's to read. Their pending
|
|
10769
10982
|
* permission prompts are retired first (`expireOrphanedPermissions`). A failed write is logged,
|
|
@@ -10774,7 +10987,11 @@ var init_manager = __esm({
|
|
|
10774
10987
|
if (ids?.length === 0) return;
|
|
10775
10988
|
await this.expireOrphanedPermissions(ids);
|
|
10776
10989
|
let write = this.deps.db.from("chat_sessions").update({ status: "idle" }).eq("runner_id", this.deps.runnerId).in("status", ORPHANED_STATUSES);
|
|
10777
|
-
if (ids)
|
|
10990
|
+
if (ids) {
|
|
10991
|
+
write = write.in("id", ids);
|
|
10992
|
+
} else if (this.live.size > 0) {
|
|
10993
|
+
write = write.not("id", "in", `(${[...this.live.keys()].join(",")})`);
|
|
10994
|
+
}
|
|
10778
10995
|
const { data, error } = await write.select("id");
|
|
10779
10996
|
if (error) {
|
|
10780
10997
|
this.deps.log.warn({ err: error.message }, "settling orphaned session rows failed");
|
|
@@ -10874,12 +11091,17 @@ var init_manager = __esm({
|
|
|
10874
11091
|
this.assertCodexOwner(sessionId, createdBy);
|
|
10875
11092
|
return { slashCommands: await this.provider(() => live.session.supportedCommands()) };
|
|
10876
11093
|
}
|
|
11094
|
+
/**
|
|
11095
|
+
* `stop` every live session, as `session.stop` would. Used at daemon shutdown and when the runner
|
|
11096
|
+
* owner leaves the runner's workspace (S-42): it sets no flag, so the manager serves again after.
|
|
11097
|
+
*/
|
|
10877
11098
|
async shutdown() {
|
|
10878
11099
|
await Promise.allSettled([...this.live.keys()].map((id) => this.stop(id)));
|
|
10879
11100
|
}
|
|
10880
11101
|
// --- internals ---
|
|
11102
|
+
/** The roster decides, the runner owner included: under 0039 an owner out of the workspace is refused. */
|
|
10881
11103
|
serves(userId) {
|
|
10882
|
-
return
|
|
11104
|
+
return this.deps.serves(userId);
|
|
10883
11105
|
}
|
|
10884
11106
|
/**
|
|
10885
11107
|
* Write this session's skills and name the tree the SDK should load (D38, lane 6 §4).
|
|
@@ -11557,7 +11779,7 @@ var init_manager = __esm({
|
|
|
11557
11779
|
const create = this.deps.createSession ?? createProviderSession;
|
|
11558
11780
|
let session;
|
|
11559
11781
|
try {
|
|
11560
|
-
session = create(provider, this.withProviderEnv(
|
|
11782
|
+
session = create(provider, this.withProviderEnv(opts, place.repo));
|
|
11561
11783
|
} catch (err) {
|
|
11562
11784
|
if (this.live.get(sessionId)?.channel !== channel) {
|
|
11563
11785
|
await this.deps.db.removeChannel(channel);
|
|
@@ -11573,7 +11795,7 @@ var init_manager = __esm({
|
|
|
11573
11795
|
channel,
|
|
11574
11796
|
relay: new SessionRelay({
|
|
11575
11797
|
sessionId,
|
|
11576
|
-
send: (
|
|
11798
|
+
send: sendOnChannel(channel),
|
|
11577
11799
|
log: this.deps.log
|
|
11578
11800
|
}),
|
|
11579
11801
|
seq,
|
|
@@ -11725,13 +11947,15 @@ var init_manager = __esm({
|
|
|
11725
11947
|
return this.changesHigh;
|
|
11726
11948
|
}
|
|
11727
11949
|
/**
|
|
11728
|
-
* The account pairing rides on the session's own env, never on `process.env` (D33).
|
|
11729
|
-
*
|
|
11730
|
-
*
|
|
11731
|
-
*
|
|
11950
|
+
* The account pairing rides on the session's own env, never on `process.env` (D33). Both
|
|
11951
|
+
* providers get it: a Codex session is owner-only, and a `claude` it runs uses the runner's
|
|
11952
|
+
* account, as in the owner's terminal (D55, amending D33). Merged over the env the
|
|
11953
|
+
* settings policy already set (D39's `CLAUDE.md` switch), never instead of it. A repository
|
|
11954
|
+
* chat's credential environment (D50, `repositoryChatEnv`) goes last, so nothing before it can
|
|
11955
|
+
* put a credential helper, a token or a `gh` login back.
|
|
11732
11956
|
*/
|
|
11733
|
-
withProviderEnv(
|
|
11734
|
-
const extra =
|
|
11957
|
+
withProviderEnv(opts, repo) {
|
|
11958
|
+
const extra = this.deps.claudeEnv;
|
|
11735
11959
|
const repoEnv = repo ? this.repositoryEnv(repo.creds) : void 0;
|
|
11736
11960
|
if ((!extra || Object.keys(extra).length === 0) && !repoEnv) return opts;
|
|
11737
11961
|
return { ...opts, env: { ...opts.env, ...extra, ...repoEnv } };
|
|
@@ -11944,10 +12168,10 @@ var init_manager = __esm({
|
|
|
11944
12168
|
/**
|
|
11945
12169
|
* Join `session:{id}` without waiting for it (chat latency follow-up, protocol §3).
|
|
11946
12170
|
*
|
|
11947
|
-
* The runner only sends on this channel, and
|
|
11948
|
-
*
|
|
11949
|
-
* the socket is down, costs relay subscribers the socket path and never the session:
|
|
11950
|
-
* this, it failed every `session.start` and every resume for as long as Realtime was down.
|
|
12171
|
+
* The runner only sends on this channel, and the relay sends a channel that cannot push over
|
|
12172
|
+
* REST (`sendOnChannel`), within the same bound. So a join that fails, or that nobody answers
|
|
12173
|
+
* while the socket is down, costs relay subscribers the socket path and never the session:
|
|
12174
|
+
* before this, it failed every `session.start` and every resume for as long as Realtime was down.
|
|
11951
12175
|
*
|
|
11952
12176
|
* A join that errors or times out is the client's to retry: it rejoins this instance on its own
|
|
11953
12177
|
* timer while the socket is up, and as soon as the socket reopens. So the instance is kept, not
|
|
@@ -11993,12 +12217,13 @@ var init_manager = __esm({
|
|
|
11993
12217
|
const safeContent = this.secrets.redact(content);
|
|
11994
12218
|
const safeRaw = this.secrets.redact(raw);
|
|
11995
12219
|
live.seq += 1;
|
|
12220
|
+
const seq = live.seq;
|
|
11996
12221
|
const { data, error } = await this.deps.db.from("chat_messages").insert({
|
|
11997
12222
|
session_id: sessionId,
|
|
11998
12223
|
// Overwritten from the session by stamp_message_workspace; the insert policy still needs
|
|
11999
12224
|
// is_member(workspace_id) to see a non-null value.
|
|
12000
12225
|
workspace_id: this.deps.workspaceId,
|
|
12001
|
-
seq
|
|
12226
|
+
seq,
|
|
12002
12227
|
role,
|
|
12003
12228
|
content: safeContent,
|
|
12004
12229
|
provider_uuid: providerUuid,
|
|
@@ -12012,7 +12237,7 @@ var init_manager = __esm({
|
|
|
12012
12237
|
}
|
|
12013
12238
|
const row = {
|
|
12014
12239
|
...data,
|
|
12015
|
-
seq
|
|
12240
|
+
seq,
|
|
12016
12241
|
role,
|
|
12017
12242
|
content: safeContent
|
|
12018
12243
|
};
|
|
@@ -12492,7 +12717,21 @@ function createTerminalChannels({
|
|
|
12492
12717
|
if (m.type === "output" && manager.relaySuppressed(terminalId)) return;
|
|
12493
12718
|
const entry = joined.get(terminalId);
|
|
12494
12719
|
if (!entry) return;
|
|
12495
|
-
|
|
12720
|
+
if (m.type !== "exit" && !canPush(entry.channel)) {
|
|
12721
|
+
if (entry.skipped === 0) {
|
|
12722
|
+
log.warn(
|
|
12723
|
+
{ terminalId, state: entry.channel.state },
|
|
12724
|
+
"terminal channel cannot push; skipping output until it can"
|
|
12725
|
+
);
|
|
12726
|
+
}
|
|
12727
|
+
entry.skipped += 1;
|
|
12728
|
+
return;
|
|
12729
|
+
}
|
|
12730
|
+
if (entry.skipped > 0 && canPush(entry.channel)) {
|
|
12731
|
+
log.info({ terminalId, skipped: entry.skipped }, "terminal channel pushing again");
|
|
12732
|
+
entry.skipped = 0;
|
|
12733
|
+
}
|
|
12734
|
+
const sent = entry.send({ type: "broadcast", event: TERMINAL_OUT_EVENT, payload: m }, BROADCAST_SEND_TIMEOUT_MS).then(
|
|
12496
12735
|
(result) => {
|
|
12497
12736
|
if (result !== "ok")
|
|
12498
12737
|
log.warn({ terminalId, type: m.type, result }, "terminal frame not sent");
|
|
@@ -12541,7 +12780,12 @@ function createTerminalChannels({
|
|
|
12541
12780
|
log.warn({ terminalId, err: messageOf5(err) }, "terminal message refused");
|
|
12542
12781
|
}
|
|
12543
12782
|
});
|
|
12544
|
-
joined.set(terminalId, {
|
|
12783
|
+
joined.set(terminalId, {
|
|
12784
|
+
channel,
|
|
12785
|
+
send: sendOnChannel(channel),
|
|
12786
|
+
pending: /* @__PURE__ */ new Set(),
|
|
12787
|
+
skipped: 0
|
|
12788
|
+
});
|
|
12545
12789
|
try {
|
|
12546
12790
|
await new Promise((resolve5, reject) => {
|
|
12547
12791
|
let settled = false;
|
|
@@ -12574,6 +12818,7 @@ var init_channels = __esm({
|
|
|
12574
12818
|
"src/terminal/channels.ts"() {
|
|
12575
12819
|
"use strict";
|
|
12576
12820
|
init_src();
|
|
12821
|
+
init_broadcast();
|
|
12577
12822
|
messageOf5 = (err) => err instanceof Error ? err.message : String(err);
|
|
12578
12823
|
}
|
|
12579
12824
|
});
|
|
@@ -12589,7 +12834,10 @@ function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
|
|
|
12589
12834
|
if (!runnerOwner && !MEMBER_ENV_KEYS.has(key) && !LOCALE_ENV.test(key)) continue;
|
|
12590
12835
|
env[key] = value;
|
|
12591
12836
|
}
|
|
12592
|
-
if (runnerOwner
|
|
12837
|
+
if (runnerOwner) {
|
|
12838
|
+
if (claudeConfigDir) env.CLAUDE_CONFIG_DIR = claudeConfigDir;
|
|
12839
|
+
else delete env.CLAUDE_CONFIG_DIR;
|
|
12840
|
+
}
|
|
12593
12841
|
return {
|
|
12594
12842
|
...env,
|
|
12595
12843
|
TERM: "xterm-256color",
|
|
@@ -12654,7 +12902,7 @@ function inside3(candidate, root) {
|
|
|
12654
12902
|
return rel === "" || !rel.startsWith("..") && !isAbsolute21(rel);
|
|
12655
12903
|
}
|
|
12656
12904
|
var SECRET_ENV, MEMBER_ENV_ALLOW, MEMBER_ENV_KEYS, LOCALE_ENV, nodeShellFs;
|
|
12657
|
-
var
|
|
12905
|
+
var init_env2 = __esm({
|
|
12658
12906
|
"src/terminal/env.ts"() {
|
|
12659
12907
|
"use strict";
|
|
12660
12908
|
init_src();
|
|
@@ -12839,7 +13087,7 @@ var init_manager2 = __esm({
|
|
|
12839
13087
|
"use strict";
|
|
12840
13088
|
init_src();
|
|
12841
13089
|
init_paths();
|
|
12842
|
-
|
|
13090
|
+
init_env2();
|
|
12843
13091
|
init_process_group();
|
|
12844
13092
|
init_refusals();
|
|
12845
13093
|
init_ring();
|
|
@@ -14031,8 +14279,8 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14031
14279
|
runnerId: identity.runnerId,
|
|
14032
14280
|
workspaceId: identity.workspaceId,
|
|
14033
14281
|
runnerUserId: identity.userId,
|
|
14034
|
-
// The roster
|
|
14035
|
-
|
|
14282
|
+
// The roster decides who may command, the owner included (0039): live, with no restart.
|
|
14283
|
+
serves: (userId) => liveRoster.serves(userId),
|
|
14036
14284
|
worktreeOwners,
|
|
14037
14285
|
sessionWorktrees,
|
|
14038
14286
|
// And which sessions were started from a task, so their secret-file guard outlives a restart.
|
|
@@ -14065,6 +14313,30 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14065
14313
|
tap: (sessionId, event) => local.emit(sessionId, event),
|
|
14066
14314
|
timings
|
|
14067
14315
|
});
|
|
14316
|
+
let ownerServed = liveRoster.serves(identity.userId);
|
|
14317
|
+
let ownerTransition = Promise.resolve();
|
|
14318
|
+
liveRoster.onRefresh(() => {
|
|
14319
|
+
const served = liveRoster.serves(identity.userId);
|
|
14320
|
+
if (served === ownerServed) return;
|
|
14321
|
+
ownerServed = served;
|
|
14322
|
+
const line = { runnerId: identity.runnerId };
|
|
14323
|
+
ownerTransition = ownerTransition.then(async () => {
|
|
14324
|
+
try {
|
|
14325
|
+
if (served) {
|
|
14326
|
+
logger.info(line, "runner owner served again; settling chats left mid-turn");
|
|
14327
|
+
await manager.settleOrphans();
|
|
14328
|
+
} else {
|
|
14329
|
+
logger.warn(line, "runner owner no longer served; stopping every live session");
|
|
14330
|
+
await manager.shutdown();
|
|
14331
|
+
}
|
|
14332
|
+
} catch (err) {
|
|
14333
|
+
logger.warn(
|
|
14334
|
+
{ ...line, served, err: err instanceof Error ? err.message : String(err) },
|
|
14335
|
+
"runner owner transition failed"
|
|
14336
|
+
);
|
|
14337
|
+
}
|
|
14338
|
+
});
|
|
14339
|
+
});
|
|
14068
14340
|
let terminals;
|
|
14069
14341
|
let terminalManager = null;
|
|
14070
14342
|
if (pty) {
|
|
@@ -14119,7 +14391,15 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14119
14391
|
terminals
|
|
14120
14392
|
})
|
|
14121
14393
|
);
|
|
14122
|
-
const source = new CommandSource(db, identity.runnerId, dispatch, logger, {
|
|
14394
|
+
const source = new CommandSource(db, identity.runnerId, dispatch, logger, {
|
|
14395
|
+
timings,
|
|
14396
|
+
// S-42 review C: a row under this id counts as this runner's only while it names this owner
|
|
14397
|
+
// in this workspace.
|
|
14398
|
+
owner: { userId: identity.userId, workspaceId: identity.workspaceId },
|
|
14399
|
+
// S-42 review A: the source hears of a removal or a re-add within a probe; the roster, left
|
|
14400
|
+
// alone, would at its next 60 s timer (a removal changes no runner row). Refresh it now.
|
|
14401
|
+
onOwnRowVisibility: () => void liveRoster.refresh()
|
|
14402
|
+
});
|
|
14123
14403
|
const heartbeat = startHeartbeat(
|
|
14124
14404
|
db,
|
|
14125
14405
|
identity.runnerId,
|
|
@@ -14174,7 +14454,8 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14174
14454
|
log: logger
|
|
14175
14455
|
}),
|
|
14176
14456
|
// The roster's own predicate, read live: the owner while a member, listed members while
|
|
14177
|
-
// still members (PR16); every refresh re-checks
|
|
14457
|
+
// still members (PR16), nobody while the owner is out (0039); every refresh re-checks
|
|
14458
|
+
// every socket (SEC-P2).
|
|
14178
14459
|
serves: (userId) => liveRoster.serves(userId),
|
|
14179
14460
|
onRosterRefresh: (cb) => liveRoster.onRefresh(cb),
|
|
14180
14461
|
// Exactly the manager's rule (ruling 14, R2-H1), built from the same deps it was given.
|