@capybara-hq/kernel 0.1.0-rc.19 → 0.1.0-rc.20
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/bootstrap.json +25 -25
- package/dist/admin/admin-agents.controller.d.ts +28 -0
- package/dist/admin/admin-agents.controller.js +79 -0
- package/dist/admin/admin-connectors.controller.d.ts +29 -0
- package/dist/admin/admin-connectors.controller.js +79 -0
- package/dist/admin/admin-items.schemas.d.ts +42 -0
- package/dist/admin/admin-items.schemas.js +48 -0
- package/dist/admin/admin.controller.d.ts +11 -20
- package/dist/admin/admin.controller.js +28 -34
- package/dist/admin/admin.module.d.ts +4 -1
- package/dist/admin/admin.module.js +50 -4
- package/dist/admin/admin.schemas.d.ts +4 -0
- package/dist/admin/admin.schemas.js +4 -1
- package/dist/admin/audit.controller.d.ts +14 -0
- package/dist/admin/audit.controller.js +42 -0
- package/dist/admin/audit.schemas.d.ts +8 -0
- package/dist/admin/audit.schemas.js +17 -0
- package/dist/admin/health.service.d.ts +104 -0
- package/dist/admin/health.service.js +246 -0
- package/dist/admin/overview.controller.d.ts +31 -0
- package/dist/admin/overview.controller.js +78 -0
- package/dist/admin/overview.schemas.d.ts +8 -0
- package/dist/admin/overview.schemas.js +13 -0
- package/dist/admin/overview.service.d.ts +112 -0
- package/dist/admin/overview.service.js +223 -0
- package/dist/admin/platform-audit.service.d.ts +41 -0
- package/dist/admin/platform-audit.service.js +70 -0
- package/dist/admin/usage.service.d.ts +149 -0
- package/dist/admin/usage.service.js +321 -0
- package/dist/agents/admin-agents.d.ts +92 -0
- package/dist/agents/admin-agents.js +170 -0
- package/dist/agents/agents.module.d.ts +4 -3
- package/dist/agents/agents.module.js +7 -3
- package/dist/agents/agents.service.d.ts +41 -3
- package/dist/agents/agents.service.js +113 -27
- package/dist/agents/definition.d.ts +16 -0
- package/dist/agents/definition.js +32 -2
- package/dist/agents/org-templates.js +4 -0
- package/dist/background/launch.d.ts +1 -1
- package/dist/background/launch.js +3 -3
- package/dist/background/tasks.service.d.ts +35 -3
- package/dist/background/tasks.service.js +91 -36
- package/dist/catalog/catalog.module.js +8 -2
- package/dist/channels/channels.module.js +11 -2
- package/dist/channels/supervisor.d.ts +2 -0
- package/dist/channels/supervisor.js +4 -0
- package/dist/conversations/events.js +1 -1
- package/dist/conversations/titles.js +4 -8
- package/dist/db/kernel-schema.d.ts +8 -1
- package/dist/devices/devices.module.js +5 -2
- package/dist/errors.d.ts +22 -2
- package/dist/errors.js +22 -2
- package/dist/evals/evals.module.js +11 -2
- package/dist/extensions/permission-view.js +3 -3
- package/dist/http/api.module.js +10 -1
- package/dist/identity/settings.js +3 -2
- package/dist/identity/write-actor.d.ts +11 -0
- package/dist/identity/write-actor.js +7 -0
- package/dist/inbox/inbox.service.d.ts +2 -1
- package/dist/inbox/platform-inbox.service.d.ts +7 -1
- package/dist/inbox/platform-inbox.service.js +20 -2
- package/dist/infra/presence/presence.module.d.ts +6 -0
- package/dist/infra/presence/presence.module.js +22 -0
- package/dist/infra/presence/replica-presence.d.ts +87 -0
- package/dist/infra/presence/replica-presence.js +189 -0
- package/dist/kernel.module.js +3 -0
- package/dist/lifecycle/kernel-lifecycle.d.ts +3 -1
- package/dist/lifecycle/kernel-lifecycle.js +17 -3
- package/dist/local-devices/local-device-gateway.d.ts +6 -0
- package/dist/local-devices/local-device-gateway.js +12 -0
- package/dist/overrides.d.ts +7 -1
- package/dist/overrides.js +4 -0
- package/dist/platform/exclusions.js +15 -0
- package/dist/platform/operation.d.ts +1 -1
- package/dist/platform/operation.js +1 -1
- package/dist/platform/ops/admin-items.ops.d.ts +37 -0
- package/dist/platform/ops/admin-items.ops.js +278 -0
- package/dist/platform/ops/admin.ops.d.ts +10 -10
- package/dist/platform/ops/admin.ops.js +111 -42
- package/dist/platform/ops/agent-skills.ops.d.ts +29 -3
- package/dist/platform/ops/agent-skills.ops.js +290 -7
- package/dist/platform/ops/inbox.ops.js +7 -4
- package/dist/platform/ops/platform.ops.js +1 -1
- package/dist/platform/ops/skills.ops.js +1 -57
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +8 -0
- package/dist/roles/gateway-role.d.ts +4 -0
- package/dist/roles/gateway-role.js +8 -0
- package/dist/runtime/run-jobs.js +4 -2
- package/dist/sandbox/sandbox-file-reader.d.ts +35 -0
- package/dist/sandbox/sandbox-file-reader.js +98 -0
- package/dist/skills/agent-skills.controller.d.ts +48 -0
- package/dist/skills/agent-skills.controller.js +131 -0
- package/dist/skills/agent-skills.service.d.ts +262 -0
- package/dist/skills/agent-skills.service.js +996 -0
- package/dist/skills/catalog.d.ts +8 -6
- package/dist/skills/catalog.js +8 -4
- package/dist/skills/changes.controller.d.ts +5 -22
- package/dist/skills/changes.controller.js +7 -55
- package/dist/skills/changes.service.d.ts +69 -115
- package/dist/skills/changes.service.js +249 -599
- package/dist/skills/conversation-catalog.d.ts +53 -0
- package/dist/skills/conversation-catalog.js +149 -0
- package/dist/skills/invocation-keys.d.ts +1 -1
- package/dist/skills/invocation-keys.js +1 -1
- package/dist/skills/package.d.ts +0 -5
- package/dist/skills/package.js +0 -14
- package/dist/skills/skills.controller.js +1 -10
- package/dist/skills/skills.module.js +15 -2
- package/dist/skills/skills.schemas.d.ts +11 -15
- package/dist/skills/skills.schemas.js +27 -20
- package/dist/skills/skills.service.d.ts +37 -30
- package/dist/skills/skills.service.js +107 -59
- package/dist/skills/usable.d.ts +4 -3
- package/dist/skills/usable.js +13 -4
- package/dist/tools/admin-connectors.d.ts +69 -0
- package/dist/tools/admin-connectors.js +135 -0
- package/dist/tools/builtin-names.d.ts +9 -14
- package/dist/tools/builtin-names.js +13 -22
- package/dist/tools/builtins/builtin-tools.d.ts +2 -3
- package/dist/tools/builtins/builtin-tools.js +21 -26
- package/dist/tools/builtins/sandbox/bash.js +2 -2
- package/dist/tools/builtins/skill-load.js +1 -0
- package/dist/tools/connectors.controller.d.ts +5 -1
- package/dist/tools/connectors.controller.js +17 -3
- package/dist/tools/connectors.service.d.ts +20 -1
- package/dist/tools/connectors.service.js +61 -0
- package/dist/tools/legacy-keys.js +0 -1
- package/dist/tools/org-tool-switches.d.ts +3 -2
- package/dist/tools/org-tool-switches.js +4 -4
- package/dist/tools/policy.d.ts +7 -9
- package/dist/tools/policy.js +11 -22
- package/dist/tools/tools.module.js +3 -0
- package/package.json +1 -1
- package/src/migrations/0109_audit_platform_idx.sql +5 -0
- package/src/migrations/0110_usage_time_idx.sql +8 -0
- package/src/migrations/0111_agent_skill_sharing.sql +22 -0
- package/src/migrations/0112_skill_share_requests.sql +4 -0
- package/dist/tools/builtins/skill-write.d.ts +0 -59
- package/dist/tools/builtins/skill-write.js +0 -244
|
@@ -135,46 +135,91 @@ let BackgroundTasks = class BackgroundTasks {
|
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
137
|
* The end of the task's output, at most `tail` bytes. Read live when the task's device
|
|
138
|
-
* instance runs (never resuming a suspended one); a finished task
|
|
139
|
-
* with the last lines its finished event kept. Anything else is
|
|
140
|
-
* `background_task.output_unavailable` (409)
|
|
138
|
+
* instance runs (never resuming a suspended one); a finished task that cannot be read live
|
|
139
|
+
* answers with the last lines its finished event kept and why (`unavailable`). Anything else is
|
|
140
|
+
* `background_task.output_unavailable` (409) with `details: [{ reason, environment }]`.
|
|
141
141
|
*/
|
|
142
142
|
async output(orgId, conversationId, accountId, taskId, tail) {
|
|
143
143
|
const task = await this.owned(orgId, conversationId, accountId, taskId);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const actor = { accountId, conversationId: task.conversationId };
|
|
152
|
-
try {
|
|
153
|
-
const r = await this.sandbox.ifRunning(task.deviceId, { createdAt }, async (p, h, signal) => {
|
|
154
|
-
const res = await actingAs(p, actor).exec(h, {
|
|
155
|
-
command: tailScript(task.id, tail),
|
|
156
|
-
cwd: rootsOf(h).workspace,
|
|
157
|
-
timeoutMs: OUTPUT_READ_TIMEOUT_MS,
|
|
158
|
-
maxOutputBytes: 2 * OUTPUT_TAIL_MAX_BYTES + 4096,
|
|
159
|
-
signal,
|
|
160
|
-
});
|
|
161
|
-
return res.exitCode === 0 ? parseTail(res.stdout, tail) : null;
|
|
162
|
-
}, AbortSignal.timeout(OUTPUT_READ_TIMEOUT_MS));
|
|
163
|
-
if (r.kind === "ran" && r.value !== null)
|
|
164
|
-
return { ...r.value, source: "live" };
|
|
165
|
-
}
|
|
166
|
-
catch (e) {
|
|
167
|
-
if (!isSandboxGoneError(e))
|
|
168
|
-
throw e;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
144
|
+
const read = await this.readLive(orgId, accountId, task, tail);
|
|
145
|
+
if (read.kind === "ok")
|
|
146
|
+
return { ...read.value, source: "live" };
|
|
147
|
+
const unavailable = {
|
|
148
|
+
reason: read.reason,
|
|
149
|
+
environment: await this.environmentOf(task.deviceId),
|
|
150
|
+
};
|
|
171
151
|
if (task.status !== "running") {
|
|
172
152
|
const ev = await this.eventLog.findByIdem(task.conversationId, finishedEventKey(task.id));
|
|
173
153
|
if (ev?.type === "background_task_finished") {
|
|
174
|
-
return { text: ev.payload.lastLines, truncated: true, source: "event" };
|
|
154
|
+
return { text: ev.payload.lastLines, truncated: true, source: "event", unavailable };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw new ApiError("background_task.output_unavailable", `The task's output cannot be read now (${unavailable.reason})`, [unavailable]);
|
|
158
|
+
}
|
|
159
|
+
/** Whether the task ran on a local device (its machine) or in a cloud sandbox. */
|
|
160
|
+
async environmentOf(deviceId) {
|
|
161
|
+
if (deviceId === null)
|
|
162
|
+
return "cloud";
|
|
163
|
+
const row = await this.db.db
|
|
164
|
+
.selectFrom("devices")
|
|
165
|
+
.select("source")
|
|
166
|
+
.where("id", "=", deviceId)
|
|
167
|
+
.executeTakeFirst();
|
|
168
|
+
return row?.source === "local" ? "local" : "cloud";
|
|
169
|
+
}
|
|
170
|
+
/** Reads the output from the task's running instance, or says why it cannot. */
|
|
171
|
+
async readLive(orgId, accountId, task, tail) {
|
|
172
|
+
if (task.sid === null || task.sandboxCreatedAt === null || task.deviceId === null) {
|
|
173
|
+
// a reservation the launch has not finished, or a launch that failed
|
|
174
|
+
return {
|
|
175
|
+
kind: "unavailable",
|
|
176
|
+
reason: task.status === "running" ? "starting" : "not_started",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (!(await this.sandbox.availableFor(task.deviceId))) {
|
|
180
|
+
return { kind: "unavailable", reason: "unavailable" };
|
|
181
|
+
}
|
|
182
|
+
if (!(await this.deviceUsable(orgId, task.conversationId))) {
|
|
183
|
+
return { kind: "unavailable", reason: "no_access" };
|
|
184
|
+
}
|
|
185
|
+
const createdAt = task.sandboxCreatedAt;
|
|
186
|
+
// the person reading it acts in the task's conversation
|
|
187
|
+
const actor = { accountId, conversationId: task.conversationId };
|
|
188
|
+
try {
|
|
189
|
+
const r = await this.sandbox.ifRunning(task.deviceId, { createdAt }, async (p, h, signal) => {
|
|
190
|
+
const res = await actingAs(p, actor).exec(h, {
|
|
191
|
+
command: tailScript(task.id, tail),
|
|
192
|
+
cwd: rootsOf(h).workspace,
|
|
193
|
+
timeoutMs: OUTPUT_READ_TIMEOUT_MS,
|
|
194
|
+
maxOutputBytes: 2 * OUTPUT_TAIL_MAX_BYTES + 4096,
|
|
195
|
+
signal,
|
|
196
|
+
});
|
|
197
|
+
return res.exitCode === 0 ? parseTail(res.stdout, tail) : null;
|
|
198
|
+
}, AbortSignal.timeout(OUTPUT_READ_TIMEOUT_MS));
|
|
199
|
+
if (r.kind === "other_instance")
|
|
200
|
+
return { kind: "unavailable", reason: "rebuilt" };
|
|
201
|
+
if (r.kind === "not_running") {
|
|
202
|
+
// a suspended instance keeps its files; any other state means they went with it
|
|
203
|
+
return { kind: "unavailable", reason: r.status === "suspended" ? "suspended" : "rebuilt" };
|
|
204
|
+
}
|
|
205
|
+
if (r.value === null)
|
|
206
|
+
return { kind: "unavailable", reason: "read_failed" };
|
|
207
|
+
if (r.value === "missing")
|
|
208
|
+
return { kind: "unavailable", reason: "output_missing" };
|
|
209
|
+
return { kind: "ok", value: r.value };
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
if (isSandboxGoneError(e))
|
|
213
|
+
return { kind: "unavailable", reason: "rebuilt" };
|
|
214
|
+
// a local device's machine: offline, or no longer the owner's (revoked, deleted)
|
|
215
|
+
if (e instanceof ApiError && e.code.startsWith("device.")) {
|
|
216
|
+
return {
|
|
217
|
+
kind: "unavailable",
|
|
218
|
+
reason: e.code === "device.offline" ? "device_offline" : "no_access",
|
|
219
|
+
};
|
|
175
220
|
}
|
|
221
|
+
throw e;
|
|
176
222
|
}
|
|
177
|
-
throw new ApiError("background_task.output_unavailable", "The task's output cannot be read now: its sandbox is not running");
|
|
178
223
|
}
|
|
179
224
|
};
|
|
180
225
|
BackgroundTasks = __decorate([
|
|
@@ -188,18 +233,28 @@ BackgroundTasks = __decorate([
|
|
|
188
233
|
__param(6, Inject(ConversationDevices))
|
|
189
234
|
], BackgroundTasks);
|
|
190
235
|
export { BackgroundTasks };
|
|
191
|
-
/**
|
|
192
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Prints `size <bytes>` and `out <base64 of the last `tail` bytes>`, or `missing` when the output
|
|
238
|
+
* file is not there. Relative to the workspace root; POSIX tools only (`wc -c`, `tail -c`,
|
|
239
|
+
* `base64`, `tr`), so it reads the same on Linux and macOS.
|
|
240
|
+
*/
|
|
241
|
+
export function tailScript(id, tail) {
|
|
193
242
|
if (!TASK_ID_RE.test(id))
|
|
194
243
|
throw new Error(`invalid background task id: ${id}`);
|
|
195
244
|
const f = `${TASKS_DIR}/${id}.out`;
|
|
196
245
|
return [
|
|
246
|
+
`if [ ! -f ${f} ]; then echo missing; exit 0; fi`,
|
|
197
247
|
`s=$({ wc -c < ${f}; } 2>/dev/null | tr -d ' '); echo "size \${s:-0}"`,
|
|
198
248
|
`printf 'out '; { tail -c ${Math.floor(tail)} ${f} 2>/dev/null | base64 | tr -d '\\n'; }; echo`,
|
|
199
249
|
].join("\n");
|
|
200
250
|
}
|
|
201
|
-
/**
|
|
202
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Decodes {@link tailScript}'s output (`missing`: no output file; null: unreadable); a cut that
|
|
253
|
+
* splits a UTF-8 character drops its tail bytes.
|
|
254
|
+
*/
|
|
255
|
+
export function parseTail(stdout, tail) {
|
|
256
|
+
if (/^missing$/m.test(stdout))
|
|
257
|
+
return "missing";
|
|
203
258
|
const size = /^size ([0-9]+)$/m.exec(stdout);
|
|
204
259
|
const out = /^out (\S*)$/m.exec(stdout);
|
|
205
260
|
if (!size || !out)
|
|
@@ -25,6 +25,10 @@ import { PlatformPluginUpdater } from "./plugin-upgrade.js";
|
|
|
25
25
|
import { CatalogRequestsService } from "./requests.service.js";
|
|
26
26
|
import { PLUGIN_SCOPE_QUEUE, PluginScopeSync } from "./scope-sync.js";
|
|
27
27
|
export { CatalogImportModule };
|
|
28
|
+
/** The plugin scope sync's queue registration (re-exported: the admin health reads its counts). */
|
|
29
|
+
const PLUGIN_SCOPE_QUEUE_MODULE = QueueModule.registerQueue(PLUGIN_SCOPE_QUEUE);
|
|
30
|
+
/** This module's maintenance queue registration (re-exported for the admin health). */
|
|
31
|
+
const MAINTENANCE_QUEUE_MODULE = QueueModule.registerQueue(MAINTENANCE_QUEUE_NAME);
|
|
28
32
|
/**
|
|
29
33
|
* The plugin catalog (spec §4, §10): {@link CatalogImportModule}, catalog requests, org installs
|
|
30
34
|
* with their dependencies, the platform plugin update (`PlatformPluginUpdater`), the admin's scope
|
|
@@ -46,10 +50,10 @@ CatalogModule = __decorate([
|
|
|
46
50
|
SkillsModule,
|
|
47
51
|
ToolsModule,
|
|
48
52
|
// the producer side of the plugin scope sync (`PluginScopeWorkersModule` consumes it)
|
|
49
|
-
|
|
53
|
+
PLUGIN_SCOPE_QUEUE_MODULE,
|
|
50
54
|
// a removed org plugin's cleanup job (`OwnedItems`, `CatalogScopeService`; consumed by the
|
|
51
55
|
// worker role's `MaintenanceProcessor`)
|
|
52
|
-
|
|
56
|
+
MAINTENANCE_QUEUE_MODULE,
|
|
53
57
|
],
|
|
54
58
|
providers: [
|
|
55
59
|
CatalogRequestsService,
|
|
@@ -64,6 +68,8 @@ CatalogModule = __decorate([
|
|
|
64
68
|
OwnedItems,
|
|
65
69
|
],
|
|
66
70
|
exports: [
|
|
71
|
+
PLUGIN_SCOPE_QUEUE_MODULE,
|
|
72
|
+
MAINTENANCE_QUEUE_MODULE,
|
|
67
73
|
CatalogImportModule,
|
|
68
74
|
CatalogRequestsService,
|
|
69
75
|
PluginInstaller,
|
|
@@ -27,6 +27,8 @@ import { ChannelInbound } from "./inbound.js";
|
|
|
27
27
|
import { CHANNEL_QUEUE_NAME, ChannelJobs } from "./jobs.js";
|
|
28
28
|
import { ChannelOutbound, deliverBackoff } from "./outbound.js";
|
|
29
29
|
import { AnonymousGuestLock, AnonymousGuests } from "./threads.js";
|
|
30
|
+
/** The channel queue's registration (its producer here; the admin health reads its counts). */
|
|
31
|
+
const CHANNEL_QUEUE_MODULE = QueueModule.registerQueue(CHANNEL_QUEUE_NAME);
|
|
30
32
|
/**
|
|
31
33
|
* Channels on every role: the org's channel connections, inbound (dedupe and enqueue; the inbound
|
|
32
34
|
* jobs), the outbound delivery, the producer of the channel queue `cap-kernel-channels`, the
|
|
@@ -48,7 +50,7 @@ ChannelsModule = __decorate([
|
|
|
48
50
|
ToolsModule,
|
|
49
51
|
ConversationsModule,
|
|
50
52
|
RuntimeModule,
|
|
51
|
-
|
|
53
|
+
CHANNEL_QUEUE_MODULE,
|
|
52
54
|
],
|
|
53
55
|
providers: [
|
|
54
56
|
ChannelJobs,
|
|
@@ -58,7 +60,14 @@ ChannelsModule = __decorate([
|
|
|
58
60
|
ChannelInbound,
|
|
59
61
|
ChannelOutbound,
|
|
60
62
|
],
|
|
61
|
-
exports: [
|
|
63
|
+
exports: [
|
|
64
|
+
ChannelJobs,
|
|
65
|
+
ChannelConnections,
|
|
66
|
+
AnonymousGuests,
|
|
67
|
+
ChannelInbound,
|
|
68
|
+
ChannelOutbound,
|
|
69
|
+
CHANNEL_QUEUE_MODULE,
|
|
70
|
+
],
|
|
62
71
|
})
|
|
63
72
|
], ChannelsModule);
|
|
64
73
|
export { ChannelsModule };
|
|
@@ -127,6 +127,8 @@ export declare class ChannelSupervisor {
|
|
|
127
127
|
* written yet on the first round).
|
|
128
128
|
*/
|
|
129
129
|
private targets;
|
|
130
|
+
/** How many long connections this process holds now (replica presence reports it). */
|
|
131
|
+
heldCount(): number;
|
|
130
132
|
private heldOf;
|
|
131
133
|
/**
|
|
132
134
|
* Gives back the most recently connected connection of `plugin`, leaving a handoff marker first;
|
|
@@ -305,6 +305,10 @@ let ChannelSupervisor = class ChannelSupervisor {
|
|
|
305
305
|
}
|
|
306
306
|
return out;
|
|
307
307
|
}
|
|
308
|
+
/** How many long connections this process holds now (replica presence reports it). */
|
|
309
|
+
heldCount() {
|
|
310
|
+
return this.held.size;
|
|
311
|
+
}
|
|
308
312
|
heldOf(plugin) {
|
|
309
313
|
let n = 0;
|
|
310
314
|
for (const h of this.held.values())
|
|
@@ -604,7 +604,7 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
|
|
|
604
604
|
/**
|
|
605
605
|
* Pinned skills (phase B, spec §9.1) in the agent's order: each a skill of `skills` (same
|
|
606
606
|
* id, version and source) with its SKILL.md body as read at run start or written by
|
|
607
|
-
*
|
|
607
|
+
* a skill registration. Absent means none (every snapshot before phase B).
|
|
608
608
|
*/
|
|
609
609
|
pinned: z
|
|
610
610
|
.array(z.object({
|
|
@@ -528,20 +528,16 @@ TitleQueue = __decorate([
|
|
|
528
528
|
__param(3, Inject(KERNEL_LOGGER))
|
|
529
529
|
], TitleQueue);
|
|
530
530
|
export { TitleQueue };
|
|
531
|
+
/** The title queue's registration (re-exported: the admin health reads its counts). */
|
|
532
|
+
const TITLE_QUEUE_MODULE = QueueModule.registerQueue(TITLE_QUEUE);
|
|
531
533
|
/** Conversation titles: the queue and the job (consumed on the worker role). */
|
|
532
534
|
let TitlesModule = class TitlesModule {
|
|
533
535
|
};
|
|
534
536
|
TitlesModule = __decorate([
|
|
535
537
|
Module({
|
|
536
|
-
imports: [
|
|
537
|
-
EventLogModule,
|
|
538
|
-
ModelsModule,
|
|
539
|
-
ExtensionsModule,
|
|
540
|
-
UsageReportModule,
|
|
541
|
-
QueueModule.registerQueue(TITLE_QUEUE),
|
|
542
|
-
],
|
|
538
|
+
imports: [EventLogModule, ModelsModule, ExtensionsModule, UsageReportModule, TITLE_QUEUE_MODULE],
|
|
543
539
|
providers: [TitleGenerator, TitleQueue],
|
|
544
|
-
exports: [TitleQueue],
|
|
540
|
+
exports: [TitleQueue, TITLE_QUEUE_MODULE],
|
|
545
541
|
})
|
|
546
542
|
], TitlesModule);
|
|
547
543
|
export { TitlesModule };
|
|
@@ -720,6 +720,10 @@ export interface SkillsTable {
|
|
|
720
720
|
platform_skill_id: ColumnType<string | null, string | null | undefined, string | null>;
|
|
721
721
|
/** 0081: a pin keeps the copy on its own stored version (ruling B2). */
|
|
722
722
|
pinned_version: ColumnType<number | null, number | null | undefined, number | null>;
|
|
723
|
+
/** 0111: the agent a shared org skill came from (cleared when that agent is deleted). */
|
|
724
|
+
origin_agent_id: ColumnType<string | null, string | null | undefined, string | null>;
|
|
725
|
+
/** 0111: the account that maintains a shared org skill. */
|
|
726
|
+
maintainer_account_id: ColumnType<string | null, string | null | undefined, string | null>;
|
|
723
727
|
}
|
|
724
728
|
/** 0024_skills.sql:技能的版本。 */
|
|
725
729
|
export interface SkillVersionsTable {
|
|
@@ -760,8 +764,11 @@ export interface SkillChangeRequestsTable {
|
|
|
760
764
|
proposed_version: Json<ProposedSkillVersion>;
|
|
761
765
|
object_prefix: string;
|
|
762
766
|
requested_by_account: string;
|
|
763
|
-
|
|
767
|
+
/** 0112: null for a share request and for a version proposal registered on the web. */
|
|
768
|
+
requested_in_conversation: string | null;
|
|
764
769
|
status: Generated<"pending" | "approved" | "rejected" | "cancelled">;
|
|
770
|
+
/** 0111: a share of an agent skill with the org, or a new version of a shared skill. */
|
|
771
|
+
kind: Dflt<"share" | "version">;
|
|
765
772
|
decided_by: string | null;
|
|
766
773
|
decided_at: TsNullable;
|
|
767
774
|
created_at: Ts;
|
|
@@ -8,6 +8,7 @@ import { Module } from "@nestjs/common";
|
|
|
8
8
|
import { ExtensionsModule } from "../extensions/extensions.module.js";
|
|
9
9
|
import { LocalDevicesModule } from "../local-devices/local-devices.module.js";
|
|
10
10
|
import { SandboxModule } from "../sandbox/sandbox.module.js";
|
|
11
|
+
import { SandboxFileReader } from "../sandbox/sandbox-file-reader.js";
|
|
11
12
|
import { DeviceAccess } from "./access.js";
|
|
12
13
|
import { ConversationDevices } from "./conversation-devices.js";
|
|
13
14
|
import { DevicesService } from "./devices.service.js";
|
|
@@ -24,8 +25,10 @@ DevicesModule = __decorate([
|
|
|
24
25
|
// ExtensionsModule: the provider display names of demoted defaults; LocalDevicesModule: deleting
|
|
25
26
|
// a local device revokes its machine
|
|
26
27
|
imports: [SandboxModule, ExtensionsModule, LocalDevicesModule],
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// SandboxFileReader lives here, not in SandboxModule: it needs ConversationDevices, and this
|
|
29
|
+
// module already imports SandboxModule (the other way would be a module cycle)
|
|
30
|
+
providers: [DeviceAccess, DevicesService, ConversationDevices, SandboxFileReader],
|
|
31
|
+
exports: [DeviceAccess, DevicesService, ConversationDevices, SandboxFileReader],
|
|
29
32
|
})
|
|
30
33
|
], DevicesModule);
|
|
31
34
|
export { DevicesModule };
|
package/dist/errors.d.ts
CHANGED
|
@@ -222,6 +222,8 @@ export declare const ERROR_CODES: {
|
|
|
222
222
|
readonly "plugin.upgrade_failed": 502;
|
|
223
223
|
readonly "setup.already_completed": 409;
|
|
224
224
|
readonly "setup.token_invalid": 403;
|
|
225
|
+
/** The admin overview's seven-day usage aggregate ran past its statement timeout (spec §5.5). */
|
|
226
|
+
readonly "admin.usage_unavailable": 503;
|
|
225
227
|
readonly "agent.not_found": 404;
|
|
226
228
|
readonly "agent.version_conflict": 409;
|
|
227
229
|
/** The profile was changed since the client read it (`expectedUpdatedAt` is stale). */
|
|
@@ -316,6 +318,14 @@ export declare const ERROR_CODES: {
|
|
|
316
318
|
readonly "attachment.too_many_pending": 429;
|
|
317
319
|
readonly "sandbox.unavailable": 503;
|
|
318
320
|
readonly "sandbox.busy": 409;
|
|
321
|
+
/** A sandbox file path the kernel reads is not absolute or not under the sandbox roots. */
|
|
322
|
+
readonly "sandbox.file_invalid_path": 422;
|
|
323
|
+
/** No file at the sandbox path the kernel reads. */
|
|
324
|
+
readonly "sandbox.file_not_found": 422;
|
|
325
|
+
/** The sandbox path the kernel reads is a directory, not one file. */
|
|
326
|
+
readonly "sandbox.file_is_directory": 422;
|
|
327
|
+
/** The sandbox file the kernel reads is over the caller's size limit. */
|
|
328
|
+
readonly "sandbox.file_too_large": 422;
|
|
319
329
|
/** The device is missing, deleted, or not the caller's (never 403, so ids cannot be probed). */
|
|
320
330
|
readonly "device.not_found": 404;
|
|
321
331
|
/** The cloud device's file system is gone at the provider; reset its files to start empty. */
|
|
@@ -355,15 +365,25 @@ export declare const ERROR_CODES: {
|
|
|
355
365
|
readonly "skill.not_found": 404;
|
|
356
366
|
/** Also a skill neither the model nor a person could use (phase D: both invocation flags off). */
|
|
357
367
|
readonly "skill.invalid_package": 400;
|
|
368
|
+
/** An agent already has `AGENT_SKILLS_MAX` own skills. */
|
|
369
|
+
readonly "skill.agent_cap": 409;
|
|
370
|
+
/** Sharing this agent skill with the org is already waiting for an org admin. */
|
|
371
|
+
readonly "skill.share_pending": 409;
|
|
372
|
+
/** Another conversation's proposed version of this shared skill is waiting for an org admin. */
|
|
373
|
+
readonly "skill.proposal_pending_elsewhere": 409;
|
|
374
|
+
/** A sub-agent conversation tried to register a skill on its agent (only the root registers). */
|
|
375
|
+
readonly "skill.subagent_register": 403;
|
|
358
376
|
/**
|
|
359
377
|
* A person invoked a skill (`/name`, phase D) that is not one of the agent's effective skills
|
|
360
378
|
* (`details: [{ reason: "not_found" }]`) or is hidden from the menu (`not_user_invocable`).
|
|
361
379
|
*/
|
|
362
380
|
readonly "skill.not_invocable": 400;
|
|
363
|
-
/**
|
|
381
|
+
/**
|
|
382
|
+
* Uploading or sharing a skill: an org-level skill with this name already exists in the org
|
|
383
|
+
* (P3-24, F18).
|
|
384
|
+
*/
|
|
364
385
|
readonly "skill.name_exists": 409;
|
|
365
386
|
readonly "skill_change.not_found": 404;
|
|
366
|
-
readonly "skill_change.not_owner": 403;
|
|
367
387
|
readonly "skill_change.already_decided": 409;
|
|
368
388
|
readonly "trajectory.not_found": 404;
|
|
369
389
|
readonly "eval.run_not_finished": 409;
|
package/dist/errors.js
CHANGED
|
@@ -222,6 +222,8 @@ export const ERROR_CODES = {
|
|
|
222
222
|
"plugin.upgrade_failed": 502,
|
|
223
223
|
"setup.already_completed": 409,
|
|
224
224
|
"setup.token_invalid": 403,
|
|
225
|
+
/** The admin overview's seven-day usage aggregate ran past its statement timeout (spec §5.5). */
|
|
226
|
+
"admin.usage_unavailable": 503,
|
|
225
227
|
"agent.not_found": 404,
|
|
226
228
|
"agent.version_conflict": 409,
|
|
227
229
|
/** The profile was changed since the client read it (`expectedUpdatedAt` is stale). */
|
|
@@ -316,6 +318,14 @@ export const ERROR_CODES = {
|
|
|
316
318
|
"attachment.too_many_pending": 429,
|
|
317
319
|
"sandbox.unavailable": 503,
|
|
318
320
|
"sandbox.busy": 409,
|
|
321
|
+
/** A sandbox file path the kernel reads is not absolute or not under the sandbox roots. */
|
|
322
|
+
"sandbox.file_invalid_path": 422,
|
|
323
|
+
/** No file at the sandbox path the kernel reads. */
|
|
324
|
+
"sandbox.file_not_found": 422,
|
|
325
|
+
/** The sandbox path the kernel reads is a directory, not one file. */
|
|
326
|
+
"sandbox.file_is_directory": 422,
|
|
327
|
+
/** The sandbox file the kernel reads is over the caller's size limit. */
|
|
328
|
+
"sandbox.file_too_large": 422,
|
|
319
329
|
/** The device is missing, deleted, or not the caller's (never 403, so ids cannot be probed). */
|
|
320
330
|
"device.not_found": 404,
|
|
321
331
|
/** The cloud device's file system is gone at the provider; reset its files to start empty. */
|
|
@@ -355,15 +365,25 @@ export const ERROR_CODES = {
|
|
|
355
365
|
"skill.not_found": 404,
|
|
356
366
|
/** Also a skill neither the model nor a person could use (phase D: both invocation flags off). */
|
|
357
367
|
"skill.invalid_package": 400,
|
|
368
|
+
/** An agent already has `AGENT_SKILLS_MAX` own skills. */
|
|
369
|
+
"skill.agent_cap": 409,
|
|
370
|
+
/** Sharing this agent skill with the org is already waiting for an org admin. */
|
|
371
|
+
"skill.share_pending": 409,
|
|
372
|
+
/** Another conversation's proposed version of this shared skill is waiting for an org admin. */
|
|
373
|
+
"skill.proposal_pending_elsewhere": 409,
|
|
374
|
+
/** A sub-agent conversation tried to register a skill on its agent (only the root registers). */
|
|
375
|
+
"skill.subagent_register": 403,
|
|
358
376
|
/**
|
|
359
377
|
* A person invoked a skill (`/name`, phase D) that is not one of the agent's effective skills
|
|
360
378
|
* (`details: [{ reason: "not_found" }]`) or is hidden from the menu (`not_user_invocable`).
|
|
361
379
|
*/
|
|
362
380
|
"skill.not_invocable": 400,
|
|
363
|
-
/**
|
|
381
|
+
/**
|
|
382
|
+
* Uploading or sharing a skill: an org-level skill with this name already exists in the org
|
|
383
|
+
* (P3-24, F18).
|
|
384
|
+
*/
|
|
364
385
|
"skill.name_exists": 409,
|
|
365
386
|
"skill_change.not_found": 404,
|
|
366
|
-
"skill_change.not_owner": 403,
|
|
367
387
|
"skill_change.already_decided": 409,
|
|
368
388
|
"trajectory.not_found": 404,
|
|
369
389
|
"eval.run_not_finished": 409,
|
|
@@ -27,6 +27,8 @@ import { EvalRunsService } from "./runs.service.js";
|
|
|
27
27
|
import { EvalSetsService } from "./sets.service.js";
|
|
28
28
|
import { TrajectoryService } from "./trajectory.service.js";
|
|
29
29
|
import { EVAL_MAX_STALLED_COUNT, EvalWorker } from "./worker.js";
|
|
30
|
+
/** The eval queue's registration (re-exported: the admin health reads its counts). */
|
|
31
|
+
const EVAL_QUEUE_MODULE = QueueModule.registerQueue(EVAL_QUEUE_NAME);
|
|
30
32
|
/**
|
|
31
33
|
* Evals: the trajectory viewer, eval sets and cases, eval runs (the producer of
|
|
32
34
|
* `cap-kernel-evals`) and the run bookkeeping ({@link EvalBudgetModule}, re-exported).
|
|
@@ -42,10 +44,17 @@ EvalsModule = __decorate([
|
|
|
42
44
|
OrgLockModule,
|
|
43
45
|
ProvidersModule,
|
|
44
46
|
EvalBudgetModule,
|
|
45
|
-
|
|
47
|
+
EVAL_QUEUE_MODULE,
|
|
46
48
|
],
|
|
47
49
|
providers: [TrajectoryService, EvalSetsService, EvalRunsService, EvalJobs],
|
|
48
|
-
exports: [
|
|
50
|
+
exports: [
|
|
51
|
+
TrajectoryService,
|
|
52
|
+
EvalSetsService,
|
|
53
|
+
EvalRunsService,
|
|
54
|
+
EvalJobs,
|
|
55
|
+
EvalBudgetModule,
|
|
56
|
+
EVAL_QUEUE_MODULE,
|
|
57
|
+
],
|
|
49
58
|
})
|
|
50
59
|
], EvalsModule);
|
|
51
60
|
export { EvalsModule };
|
|
@@ -129,9 +129,9 @@ export function permissionGroups(set) {
|
|
|
129
129
|
// older snapshots lack `ui`, `ui.components` and `client` (read as empty / false)
|
|
130
130
|
const ui = p.ui ?? { views: [], embeds: [], components: [] };
|
|
131
131
|
const components = ui.components ?? [];
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
// Level-prefixed like the config group: a field at both levels (an overridable secret such as
|
|
133
|
+
// `appSecret`) is two items, not one name listed twice.
|
|
134
|
+
const secretFields = ["deployment", "org"].flatMap((level) => set.config[level].filter((f) => f.secret).map((f) => `${level}:${f.path || "<root>"}`));
|
|
135
135
|
const configFields = ["deployment", "org"].flatMap((level) => set.config[level].map((f) => `${level}:${f.path || "<root>"}`));
|
|
136
136
|
const secretItems = [...(p.secrets ? ["secret store"] : []), ...secretFields];
|
|
137
137
|
const groups = [
|
package/dist/http/api.module.js
CHANGED
|
@@ -8,7 +8,11 @@ import { Module } from "@nestjs/common";
|
|
|
8
8
|
import { APP_GUARD } from "@nestjs/core";
|
|
9
9
|
import { AdminController } from "../admin/admin.controller.js";
|
|
10
10
|
import { AdminModule } from "../admin/admin.module.js";
|
|
11
|
+
import { AdminAgentsController } from "../admin/admin-agents.controller.js";
|
|
12
|
+
import { AdminConnectorsController } from "../admin/admin-connectors.controller.js";
|
|
13
|
+
import { AdminAuditController } from "../admin/audit.controller.js";
|
|
11
14
|
import { AdminExtensionsController } from "../admin/extensions.controller.js";
|
|
15
|
+
import { AdminOverviewController } from "../admin/overview.controller.js";
|
|
12
16
|
import { SetupController } from "../admin/setup.controller.js";
|
|
13
17
|
import { AgentsController } from "../agents/agents.controller.js";
|
|
14
18
|
import { AgentsModule } from "../agents/agents.module.js";
|
|
@@ -67,7 +71,8 @@ import { ConversationStreamController } from "../runtime/stream.controller.js";
|
|
|
67
71
|
import { SetupModule } from "../setup/setup.module.js";
|
|
68
72
|
import { SetupChecksController } from "../setup/setup-checks.controller.js";
|
|
69
73
|
import { SetupGuidesController } from "../setup/setup-guides.controller.js";
|
|
70
|
-
import { AgentSkillsController
|
|
74
|
+
import { AgentSkillsController } from "../skills/agent-skills.controller.js";
|
|
75
|
+
import { InboxController, SkillChangesController } from "../skills/changes.controller.js";
|
|
71
76
|
import { InvocableSkillsController } from "../skills/invocable.controller.js";
|
|
72
77
|
import { SkillsController } from "../skills/skills.controller.js";
|
|
73
78
|
import { SkillsModule } from "../skills/skills.module.js";
|
|
@@ -96,6 +101,10 @@ const CONTROLLERS = [
|
|
|
96
101
|
InvitationsController,
|
|
97
102
|
SetupController,
|
|
98
103
|
AdminController,
|
|
104
|
+
AdminAgentsController,
|
|
105
|
+
AdminConnectorsController,
|
|
106
|
+
AdminAuditController,
|
|
107
|
+
AdminOverviewController,
|
|
99
108
|
AdminExtensionsController,
|
|
100
109
|
AdminCatalogController,
|
|
101
110
|
AdminComponentsController,
|
|
@@ -132,8 +132,9 @@ export const DEPLOYMENT_SETTINGS = z.object({
|
|
|
132
132
|
/** 组织能否自建连接器(不经平台目录)。 */
|
|
133
133
|
customConnectors: CustomPolicy.default("allow"),
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
135
|
+
* Whether the org can create its own skills (not through the platform catalog): on deny org admins
|
|
136
|
+
* cannot upload skill packages. An agent registering its own skill (`agents.register-skill`) is not
|
|
137
|
+
* governed by it; only sharing a skill to the org is refused.
|
|
137
138
|
*/
|
|
138
139
|
customSkills: CustomPolicy.default("allow"),
|
|
139
140
|
/**
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Principal } from "./principal.js";
|
|
2
|
+
/**
|
|
3
|
+
* Who performs a write on the platform's behalf (a super admin acting on an org they need not be
|
|
4
|
+
* a member of): the account and, for a write made with a personal access token, the token id.
|
|
5
|
+
*/
|
|
6
|
+
export interface WriteActor {
|
|
7
|
+
accountId: string;
|
|
8
|
+
tokenId?: string;
|
|
9
|
+
}
|
|
10
|
+
/** The writer of a request: its user and, for a personal access token, the token id. */
|
|
11
|
+
export declare function writeActorOf(principal: Principal): WriteActor;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { patTokenId, requireUser } from "./principal.js";
|
|
2
|
+
/** The writer of a request: its user and, for a personal access token, the token id. */
|
|
3
|
+
export function writeActorOf(principal) {
|
|
4
|
+
const user = requireUser(principal);
|
|
5
|
+
const tokenId = patTokenId(user);
|
|
6
|
+
return { accountId: user.account.id, ...(tokenId !== undefined && { tokenId }) };
|
|
7
|
+
}
|
|
@@ -38,7 +38,8 @@ export declare const INBOX_MAX_ITEMS = 100;
|
|
|
38
38
|
/**
|
|
39
39
|
* 收件箱里的一项(按 `type` 区分,形状稳定):
|
|
40
40
|
* - `tool_approval`:当前用户能处理的待处理工具调用审批(与 `GET /approvals?status=pending` 同一视图);
|
|
41
|
-
* - `skill_change
|
|
41
|
+
* - `skill_change`: pending skill share requests and proposed versions of shared skills, when the
|
|
42
|
+
* current user is an org admin (`skillChange.kind`; spec §5.4, §5.5);
|
|
42
43
|
* - `catalog_request`: pending `enable` requests from members and pending `connector_setup`
|
|
43
44
|
* requests (`request.kind`; `request.agent` and `request.connectors` set, `item` null; ruling
|
|
44
45
|
* P4-T6-5), when the current user is an org admin (M6 T26, C22; plugin scope spec §5.1);
|
|
@@ -91,6 +91,10 @@ export type PlatformInboxItem = {
|
|
|
91
91
|
createdAt: string;
|
|
92
92
|
notice: PlatformNoticeView;
|
|
93
93
|
};
|
|
94
|
+
/** Every {@link PlatformInboxItem} type, in the order the admin overview lists them. */
|
|
95
|
+
export declare const PLATFORM_INBOX_TYPES: readonly ["catalog_request", "permission_confirmation", "org_upload_pending", "scope_sync_failed", "platform_migration", "ownership_transferred"];
|
|
96
|
+
/** How many items of each type wait: every type, zeros included. */
|
|
97
|
+
export type PlatformInboxCounts = Record<PlatformInboxItem["type"], number>;
|
|
94
98
|
/** An open platform notice (`platform_notices`, kind `ownership_transferred`). */
|
|
95
99
|
export interface PlatformNoticeView {
|
|
96
100
|
id: string;
|
|
@@ -100,12 +104,14 @@ export interface PlatformNoticeView {
|
|
|
100
104
|
}
|
|
101
105
|
/**
|
|
102
106
|
* `GET /admin/inbox`: newest first, at most {@link PLATFORM_INBOX_MAX_ITEMS}; `truncated`: more;
|
|
103
|
-
* `total`: how many items wait in all (also `GET /admin/inbox/count`)
|
|
107
|
+
* `total`: how many items wait in all (also `GET /admin/inbox/count`); `byType`: `total` per
|
|
108
|
+
* type, counted over everything waiting (not the listed window).
|
|
104
109
|
*/
|
|
105
110
|
export interface PlatformInboxView {
|
|
106
111
|
items: PlatformInboxItem[];
|
|
107
112
|
truncated: boolean;
|
|
108
113
|
total: number;
|
|
114
|
+
byType: PlatformInboxCounts;
|
|
109
115
|
}
|
|
110
116
|
/**
|
|
111
117
|
* The super admin's inbox (plugin scope spec §5.1 rule 5, §13.5): what waits on the platform,
|
|
@@ -24,6 +24,15 @@ export const SCOPE_SYNC_FAILURES_LIST_MAX = 200;
|
|
|
24
24
|
export const MIGRATION_RUN_STUCK_MS = 60 * 60_000;
|
|
25
25
|
/** Plugin install statuses of a plugin that is on in the org. */
|
|
26
26
|
const LIVE_INSTALL = ["installing", "installed", "upgrading"];
|
|
27
|
+
/** Every {@link PlatformInboxItem} type, in the order the admin overview lists them. */
|
|
28
|
+
export const PLATFORM_INBOX_TYPES = [
|
|
29
|
+
"catalog_request",
|
|
30
|
+
"permission_confirmation",
|
|
31
|
+
"org_upload_pending",
|
|
32
|
+
"scope_sync_failed",
|
|
33
|
+
"platform_migration",
|
|
34
|
+
"ownership_transferred",
|
|
35
|
+
];
|
|
27
36
|
/**
|
|
28
37
|
* The super admin's inbox (plugin scope spec §5.1 rule 5, §13.5): what waits on the platform,
|
|
29
38
|
* gathered from the sources the admin pages read (the 待确认 tab's requests and permission
|
|
@@ -96,9 +105,18 @@ let PlatformInboxService = class PlatformInboxService {
|
|
|
96
105
|
: a.id > b.id
|
|
97
106
|
? 1
|
|
98
107
|
: 0);
|
|
99
|
-
const
|
|
108
|
+
const orgUploads = pending.filter((i) => i.owner.kind === "org").length;
|
|
109
|
+
const byType = {
|
|
110
|
+
catalog_request: requestCount,
|
|
111
|
+
permission_confirmation: pending.length - orgUploads,
|
|
112
|
+
org_upload_pending: orgUploads,
|
|
113
|
+
scope_sync_failed: failureCount,
|
|
114
|
+
platform_migration: run === null ? 0 : 1,
|
|
115
|
+
ownership_transferred: noticeCount,
|
|
116
|
+
};
|
|
117
|
+
const total = Object.values(byType).reduce((a, b) => a + b, 0);
|
|
100
118
|
const items = all.slice(0, PLATFORM_INBOX_MAX_ITEMS);
|
|
101
|
-
return { items, truncated: total > items.length, total };
|
|
119
|
+
return { items, truncated: total > items.length, total, byType };
|
|
102
120
|
}
|
|
103
121
|
/**
|
|
104
122
|
* How many items wait (the sidebar's count): the same sources, counted where they can be (the
|