@ran-sh/dsh-crew 0.5.7 → 1.0.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/README.md +6 -0
- package/README.zh.md +6 -0
- package/lib/client.js +223 -9
- package/official-web-bridge/lib/client.js +223 -9
- package/package.json +1 -1
- package/src/client/host-readiness.mjs +16 -3
- package/src/client/index.tsx +109 -8
- package/src/config-readiness.mjs +10 -0
- package/src/credential-lifecycle.mjs +73 -0
- package/src/credential-purge-adapters.mjs +127 -0
- package/src/credential-purge-state.mjs +59 -0
- package/src/credential-reference-inventory.mjs +78 -0
- package/src/dsh-cli-runtime.mjs +36 -9
- package/src/extension-contract.mjs +10 -8
- package/src/hub/index.mjs +177 -20
- package/src/hub-client.mjs +1 -1
- package/src/install/npx-lifecycle.mjs +241 -25
- package/src/job-contracts.mjs +9 -4
- package/src/official-web-bridge.mjs +6 -11
- package/src/provider-delete-adapters.mjs +28 -1
- package/src/provider-desired-state.mjs +37 -0
- package/src/provider-lifecycle-state.mjs +15 -1
- package/src/runtime-identity.mjs +24 -9
- package/src/runtime-readiness-snapshot.mjs +87 -0
- package/src/server.mjs +30 -5
package/src/hub/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// and serves the one-click installer endpoints for the settings page.
|
|
5
5
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { join, isAbsolute } from 'node:path';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { createShardWriter, readMergedStatus } from '../status-shard.mjs';
|
|
@@ -39,6 +39,11 @@ import { normalizeProviderLifecycleState } from '../provider-lifecycle-state.mjs
|
|
|
39
39
|
import { createProviderHealthStore } from '../provider-health.mjs';
|
|
40
40
|
import { planProviderDelete } from '../provider-lifecycle.mjs';
|
|
41
41
|
import { createProviderDeleteFileHooks } from '../provider-delete-adapters.mjs';
|
|
42
|
+
import { buildCredentialReferenceInventory } from '../credential-reference-inventory.mjs';
|
|
43
|
+
import { executeCredentialPurge, planCredentialPurge } from '../credential-lifecycle.mjs';
|
|
44
|
+
import { createCredentialPurgeFileHooks } from '../credential-purge-adapters.mjs';
|
|
45
|
+
import { normalizeCredentialPurgeState, recordCredentialPurgeOutcome } from '../credential-purge-state.mjs';
|
|
46
|
+
import { buildRuntimeReadinessSnapshot } from '../runtime-readiness-snapshot.mjs';
|
|
42
47
|
|
|
43
48
|
// policy.mjs is pure (no @deepseek-ai imports, no ctx access), so importing it
|
|
44
49
|
// here is safe for the profile-realm discipline: it never pulls in package
|
|
@@ -100,6 +105,7 @@ const LEGACY_TIER_MODELS = { flash: 'deepseek-v4-flash', pro: 'deepseek-v4-pro'
|
|
|
100
105
|
// the profile realm): a valid dispatch role set.
|
|
101
106
|
const ROLES = { worker: true, reviewer: true };
|
|
102
107
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
108
|
+
const CREDENTIAL_PURGE_STATE_FILE = join(CONFIG_DIR, 'credential-purge-lifecycle.json');
|
|
103
109
|
|
|
104
110
|
function readProviderLifecycleState() {
|
|
105
111
|
try {
|
|
@@ -109,6 +115,22 @@ function readProviderLifecycleState() {
|
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
function readCredentialPurgeState() {
|
|
119
|
+
try { return normalizeCredentialPurgeState(JSON.parse(readFileSync(CREDENTIAL_PURGE_STATE_FILE, 'utf8'))); } catch { return normalizeCredentialPurgeState(); }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeCredentialPurgeState(state) {
|
|
123
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
124
|
+
const temp = `${CREDENTIAL_PURGE_STATE_FILE}.${process.pid}.${Date.now()}.tmp`;
|
|
125
|
+
try {
|
|
126
|
+
writeFileSync(temp, JSON.stringify(normalizeCredentialPurgeState(state), null, 2) + '\n', 'utf8');
|
|
127
|
+
renameSync(temp, CREDENTIAL_PURGE_STATE_FILE);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
try { rmSync(temp, { force: true }); } catch {}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
function providerInventoryPolicy(config) {
|
|
113
135
|
return {
|
|
114
136
|
worker: config?.worker?.model_policy ?? {
|
|
@@ -140,13 +162,31 @@ async function readProviderInventorySnapshot(hub, ctx, config) {
|
|
|
140
162
|
}
|
|
141
163
|
} catch {}
|
|
142
164
|
const lifecycle = readProviderLifecycleState();
|
|
143
|
-
|
|
165
|
+
const credentialPurge = readCredentialPurgeState();
|
|
166
|
+
const inventory = buildProviderInventory({
|
|
144
167
|
catalog,
|
|
145
168
|
declarations,
|
|
146
169
|
policy: providerInventoryPolicy(config),
|
|
147
170
|
tombstones: lifecycle.tombstones,
|
|
148
171
|
activeJobs: hub.list(),
|
|
149
172
|
});
|
|
173
|
+
return {
|
|
174
|
+
...inventory,
|
|
175
|
+
credential_history_refs: Object.values(lifecycle.transactions ?? {})
|
|
176
|
+
.flatMap((transaction) => Array.isArray(transaction?.credential_refs) ? transaction.credential_refs : []),
|
|
177
|
+
credential_purged_refs: Object.keys(credentialPurge.purged),
|
|
178
|
+
credential_purge_unverified: Object.entries(credentialPurge.unverified).map(([referenceId, record]) => ({
|
|
179
|
+
reference_id: referenceId,
|
|
180
|
+
transaction_id: record.transaction_id,
|
|
181
|
+
state: record.state,
|
|
182
|
+
})),
|
|
183
|
+
lifecycle_transactions: Object.entries(lifecycle.transactions ?? {}).slice(-64).map(([transactionId, transaction]) => ({
|
|
184
|
+
transaction_id: transactionId,
|
|
185
|
+
provider_id: transaction.provider_id,
|
|
186
|
+
state: transaction.state,
|
|
187
|
+
...(transaction.expected_revision ? { expected_revision: transaction.expected_revision } : {}),
|
|
188
|
+
})),
|
|
189
|
+
};
|
|
150
190
|
}
|
|
151
191
|
|
|
152
192
|
function readProviderProfileRevision() {
|
|
@@ -165,12 +205,26 @@ export function hubCanonicalEvents(job = {}) {
|
|
|
165
205
|
const role = job.role === 'reviewer' ? 'reviewer' : 'worker';
|
|
166
206
|
const atStart = job.startedAt ?? null;
|
|
167
207
|
const atEnd = job.endedAt ?? atStart;
|
|
168
|
-
const definitions = [
|
|
169
|
-
['job.created', atStart, { client_job_id: job.client_job_id ?? null }],
|
|
170
|
-
['job.started', atStart, {}],
|
|
171
|
-
['
|
|
172
|
-
|
|
173
|
-
|
|
208
|
+
const definitions = [
|
|
209
|
+
['job.created', atStart, { client_job_id: job.client_job_id ?? null }],
|
|
210
|
+
['job.started', atStart, {}],
|
|
211
|
+
['runtime.bound', atStart, {
|
|
212
|
+
execution_plane: job.execution_context?.execution_plane ?? null,
|
|
213
|
+
profile: job.execution_context?.profile ?? null,
|
|
214
|
+
listen_port: Number.isInteger(job.execution_context?.listen_port) ? job.execution_context.listen_port : null,
|
|
215
|
+
runtime_id: job.execution_context?.runtime_id ?? null,
|
|
216
|
+
}],
|
|
217
|
+
['model.selected', atStart, { provider: job.provider ?? null, model: job.model ?? null, source: job.selection_source ?? null }],
|
|
218
|
+
['model.admitted', atStart, {
|
|
219
|
+
provider: job.provider ?? null,
|
|
220
|
+
model: job.model ?? null,
|
|
221
|
+
health_state: job.health_state ?? 'unprobed',
|
|
222
|
+
reason_code: job.health_reason_code ?? null,
|
|
223
|
+
observed_at: Number.isFinite(job.health_observed_at) ? job.health_observed_at : null,
|
|
224
|
+
}],
|
|
225
|
+
[role === 'reviewer' ? 'review.started' : 'worker.started', atStart, { run_id: job.id }],
|
|
226
|
+
['agent.created', atStart, { run_id: job.id }],
|
|
227
|
+
];
|
|
174
228
|
if (job.status !== 'running') {
|
|
175
229
|
definitions.push([
|
|
176
230
|
role === 'reviewer' ? 'review.completed' : 'worker.completed',
|
|
@@ -319,7 +373,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
319
373
|
* `role` (worker | reviewer) records who does the work; `tier` remains the
|
|
320
374
|
* legacy model-class slot. Reviewer-role jobs always use the pro slot.
|
|
321
375
|
*/
|
|
322
|
-
async spawn({ task, tier = 'flash', role, attempt = 0, effort = 'max', cwd, source = 'api', preset, delivery = 'coding', client_job_id, requested_isolation, workspace_branch, timeout_seconds, profile_id, workspace_context, allow_no_changes }) {
|
|
376
|
+
async spawn({ task, tier = 'flash', role, attempt = 0, effort = 'max', cwd, source = 'api', preset, delivery = 'coding', client_job_id, requested_isolation, workspace_branch, timeout_seconds, profile_id, workspace_context, allow_no_changes, ingress = 'direct-3210' }) {
|
|
323
377
|
// role is only honored when the caller explicitly names it; a legacy
|
|
324
378
|
// tier-only spawn (role === undefined) keeps the exact v0.1 resolution.
|
|
325
379
|
const hasRole = role === 'worker' || role === 'reviewer';
|
|
@@ -455,6 +509,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
455
509
|
profile: runtime.profile,
|
|
456
510
|
listen_port: runtime.listen_port,
|
|
457
511
|
runtime_id: runtime.runtime_id,
|
|
512
|
+
ingress: ingress === 'official-3080' ? 'official-3080' : 'direct-3210',
|
|
458
513
|
};
|
|
459
514
|
})(),
|
|
460
515
|
allow_no_changes: allow_no_changes === true,
|
|
@@ -895,10 +950,15 @@ export async function apply(ctx) {
|
|
|
895
950
|
const disposers = [];
|
|
896
951
|
const PROVIDER_DELETE_PLAN_TTL_MS = 10 * 60 * 1000;
|
|
897
952
|
const providerDeletePlans = new Map();
|
|
953
|
+
const credentialPurgePlans = new Map();
|
|
898
954
|
const rememberProviderDeletePlan = (plan) => {
|
|
899
955
|
providerDeletePlans.set(plan.plan_id, { plan, created_at: Date.now() });
|
|
900
956
|
while (providerDeletePlans.size > 32) providerDeletePlans.delete(providerDeletePlans.keys().next().value);
|
|
901
957
|
};
|
|
958
|
+
const rememberCredentialPurgePlan = (plan) => {
|
|
959
|
+
credentialPurgePlans.set(plan.plan_id, { plan, created_at: Date.now() });
|
|
960
|
+
while (credentialPurgePlans.size > 32) credentialPurgePlans.delete(credentialPurgePlans.keys().next().value);
|
|
961
|
+
};
|
|
902
962
|
|
|
903
963
|
// Multimodal bridge: register describe_image / generate_image for the DS
|
|
904
964
|
// model. Config is read per call so settings-page edits apply live; the
|
|
@@ -987,7 +1047,8 @@ export async function apply(ctx) {
|
|
|
987
1047
|
// as its raw default (pro-only + no tier used to spawn flash).
|
|
988
1048
|
const resolved = resolveHubSpawnPayload(payload, () => hub.getConfig?.() ?? {});
|
|
989
1049
|
if (!resolved.ok) return sendJson(res, 400, { ok: false, error: resolved.error, code: resolved.code });
|
|
990
|
-
const
|
|
1050
|
+
const ingress = req.headers?.['x-dsh-crew-ingress'] === 'official-3080' ? 'official-3080' : 'direct-3210';
|
|
1051
|
+
const job = await hub.spawn({ ...resolved.payload, ingress });
|
|
991
1052
|
return sendJson(res, 200, { ok: true, job: hub.view(job) });
|
|
992
1053
|
}
|
|
993
1054
|
if (req.method === 'POST' && parts.length === 2 && parts[1] === 'cancel') {
|
|
@@ -1067,16 +1128,27 @@ export async function apply(ctx) {
|
|
|
1067
1128
|
const workspaceReadiness = requestedWorkspaceId && !requestedContext
|
|
1068
1129
|
? { status: 'UNAVAILABLE', reason_code: 'WORKSPACE_CONTEXT_NOT_FOUND' }
|
|
1069
1130
|
: await assessWorkspaceReadiness({ cwd: requestedContext?.repo_root ?? null });
|
|
1070
|
-
const liveJobs = typeof hub.list === 'function' ? hub.list() : [];
|
|
1071
|
-
const [modelExecution, reviewerExecution] = buildHubExecutionRows(liveJobs);
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1131
|
+
const liveJobs = typeof hub.list === 'function' ? hub.list() : [];
|
|
1132
|
+
const [modelExecution, reviewerExecution] = buildHubExecutionRows(liveJobs);
|
|
1133
|
+
const readinessMatrix = { rows: [
|
|
1134
|
+
{ id: 'hub_compatibility', status: 'PASS', reason_code: 'LIVE_CHECK_PASSED' },
|
|
1135
|
+
catalogStatus,
|
|
1136
|
+
modelExecution,
|
|
1137
|
+
reviewerExecution,
|
|
1138
|
+
{ id: 'provider_lifecycle_consistent', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE' },
|
|
1139
|
+
] };
|
|
1140
|
+
const readinessSnapshot = buildRuntimeReadinessSnapshot({
|
|
1141
|
+
runtime: getHubRuntimeIdentity(),
|
|
1142
|
+
readinessMatrix,
|
|
1143
|
+
selections: { worker: liveJobs.find((job) => job?.role === 'worker') ?? null, reviewer: liveJobs.find((job) => job?.role === 'reviewer') ?? null },
|
|
1144
|
+
health: hub.healthStore.list(),
|
|
1145
|
+
jobs: liveJobs,
|
|
1146
|
+
workspace: workspaceReadiness,
|
|
1147
|
+
});
|
|
1148
|
+
const contract = buildExtensionContract({
|
|
1149
|
+
config,
|
|
1150
|
+
readinessMatrix,
|
|
1151
|
+
readinessSnapshot,
|
|
1080
1152
|
workspace: workspaceReadiness,
|
|
1081
1153
|
profiles,
|
|
1082
1154
|
runtime: getHubRuntimeIdentity(),
|
|
@@ -1195,6 +1267,91 @@ export async function apply(ctx) {
|
|
|
1195
1267
|
},
|
|
1196
1268
|
}));
|
|
1197
1269
|
|
|
1270
|
+
// Credential references are a separate, secret-free inventory. Provider
|
|
1271
|
+
// deletion can report these references, but never implies their purge.
|
|
1272
|
+
disposers.push(webServer.register({
|
|
1273
|
+
kind: 'exact', path: `${ROUTE_BASE}/credential-references`,
|
|
1274
|
+
handler: async (req, res) => {
|
|
1275
|
+
if (!isLoopbackRequest(req)) return sendJson(res, 403, { ok: false, error: 'loopback only' });
|
|
1276
|
+
if (req.method !== 'GET') return sendJson(res, 405, { ok: false, error: 'GET only' }, { allow: 'GET' });
|
|
1277
|
+
try {
|
|
1278
|
+
const config = normalizeGlobalConfig(hub.getConfig?.() ?? {});
|
|
1279
|
+
const snapshot = await readProviderInventorySnapshot(hub, ctx, config);
|
|
1280
|
+
const credentials = buildCredentialReferenceInventory({ providers: snapshot.records, additional_refs: snapshot.credential_history_refs, purged_refs: snapshot.credential_purged_refs });
|
|
1281
|
+
return sendJson(res, 200, { ok: true, ...credentials, unverified_purges: snapshot.credential_purge_unverified, runtime: getHubRuntimeIdentity() });
|
|
1282
|
+
} catch {
|
|
1283
|
+
return sendJson(res, 503, { ok: false, code: 'CREDENTIAL_REFERENCE_INVENTORY_UNAVAILABLE' });
|
|
1284
|
+
}
|
|
1285
|
+
},
|
|
1286
|
+
}));
|
|
1287
|
+
|
|
1288
|
+
disposers.push(webServer.register({
|
|
1289
|
+
kind: 'prefix', path: `${ROUTE_BASE}/credential-references`,
|
|
1290
|
+
handler: async (req, res) => {
|
|
1291
|
+
if (!isLoopbackRequest(req)) return sendJson(res, 403, { ok: false, error: 'loopback only' });
|
|
1292
|
+
let url;
|
|
1293
|
+
try { url = new URL(req.url, 'http://localhost'); } catch { return sendJson(res, 400, { ok: false, code: 'CREDENTIAL_PURGE_PLAN_INVALID' }); }
|
|
1294
|
+
const parts = url.pathname.slice(`${ROUTE_BASE}/credential-references`.length).split('/').filter(Boolean);
|
|
1295
|
+
try {
|
|
1296
|
+
const referenceId = decodeURIComponent(parts[0] ?? '');
|
|
1297
|
+
const body = await readBody(req);
|
|
1298
|
+
const config = normalizeGlobalConfig(hub.getConfig?.() ?? {});
|
|
1299
|
+
const snapshot = await readProviderInventorySnapshot(hub, ctx, config);
|
|
1300
|
+
const credentials = buildCredentialReferenceInventory({ providers: snapshot.records, additional_refs: snapshot.credential_history_refs, purged_refs: snapshot.credential_purged_refs });
|
|
1301
|
+
if (req.method === 'POST' && parts.length === 2 && parts[1] === 'purge-plan') {
|
|
1302
|
+
const planned = planCredentialPurge({ inventory: credentials, referenceId, expectedRevision: body?.expected_revision });
|
|
1303
|
+
if (!planned.ok) return sendJson(res, 409, { ok: false, code: planned.code });
|
|
1304
|
+
rememberCredentialPurgePlan(planned.plan);
|
|
1305
|
+
return sendJson(res, 200, { ok: true, plan: planned.plan });
|
|
1306
|
+
}
|
|
1307
|
+
if (req.method === 'DELETE' && parts.length === 1) {
|
|
1308
|
+
if (body?.confirm !== true) return sendJson(res, 400, { ok: false, code: 'CREDENTIAL_PURGE_CONFIRM_REQUIRED' });
|
|
1309
|
+
const stored = credentialPurgePlans.get(body?.plan_id);
|
|
1310
|
+
if (!stored || Date.now() - stored.created_at > PROVIDER_DELETE_PLAN_TTL_MS || stored.plan.reference_id !== referenceId) {
|
|
1311
|
+
credentialPurgePlans.delete(body?.plan_id);
|
|
1312
|
+
return sendJson(res, 409, { ok: false, code: 'CREDENTIAL_PURGE_PLAN_EXPIRED' });
|
|
1313
|
+
}
|
|
1314
|
+
const current = planCredentialPurge({ inventory: credentials, referenceId, expectedRevision: stored.plan.expected_revision });
|
|
1315
|
+
if (!current.ok) return sendJson(res, 409, { ok: false, code: current.code });
|
|
1316
|
+
if (current.plan.expected_revision !== stored.plan.expected_revision) return sendJson(res, 409, { ok: false, code: 'CREDENTIAL_REFERENCE_CHANGED' });
|
|
1317
|
+
credentialPurgePlans.delete(body.plan_id);
|
|
1318
|
+
const adapter = createCredentialPurgeFileHooks({
|
|
1319
|
+
credentialsFile: join(CONFIG_DIR, 'harness', '.credentials.yaml'),
|
|
1320
|
+
crewHome: join(CONFIG_DIR, 'harness'),
|
|
1321
|
+
});
|
|
1322
|
+
const result = await executeCredentialPurge(current.plan, {
|
|
1323
|
+
recheck: async (id) => {
|
|
1324
|
+
const freshSnapshot = await readProviderInventorySnapshot(hub, ctx, config);
|
|
1325
|
+
const freshCredentials = buildCredentialReferenceInventory({ providers: freshSnapshot.records, additional_refs: freshSnapshot.credential_history_refs, purged_refs: freshSnapshot.credential_purged_refs });
|
|
1326
|
+
const freshPlan = planCredentialPurge({ inventory: freshCredentials, referenceId: id });
|
|
1327
|
+
if (!freshPlan.ok) return { ok: false };
|
|
1328
|
+
const freshRecord = freshCredentials.records.find((entry) => entry.reference_id === id);
|
|
1329
|
+
return {
|
|
1330
|
+
ok: true,
|
|
1331
|
+
revision: freshPlan.plan.expected_revision,
|
|
1332
|
+
orphan: freshRecord?.orphan === true,
|
|
1333
|
+
ownership: freshRecord?.ownership,
|
|
1334
|
+
purge_capability: freshRecord?.purge_capability,
|
|
1335
|
+
};
|
|
1336
|
+
},
|
|
1337
|
+
...(adapter.ok ? { purge: adapter.purge, verify: adapter.verify } : {}),
|
|
1338
|
+
});
|
|
1339
|
+
if (result.state === 'PURGED' || result.state === 'VERIFIED') {
|
|
1340
|
+
try {
|
|
1341
|
+
writeCredentialPurgeState(recordCredentialPurgeOutcome(readCredentialPurgeState(), current.plan, result));
|
|
1342
|
+
} catch {
|
|
1343
|
+
return sendJson(res, 503, { ok: false, code: 'CREDENTIAL_PURGE_AUDIT_FAILED', state: result.state, reference_id: referenceId });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return sendJson(res, result.ok ? 200 : 503, result);
|
|
1347
|
+
}
|
|
1348
|
+
return sendJson(res, 404, { ok: false, code: 'CREDENTIAL_REFERENCE_ENDPOINT_NOT_FOUND' });
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
return sendJson(res, 400, { ok: false, code: boundedMachineCodeFromError(error) ?? 'CREDENTIAL_PURGE_PLAN_INVALID' });
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
}));
|
|
1354
|
+
|
|
1198
1355
|
// Deletion is two-phase: this endpoint only computes a revision-bound,
|
|
1199
1356
|
// secret-free impact plan. A later mutation endpoint must present that
|
|
1200
1357
|
// plan and perform restart/verification through an owned supervisor.
|
package/src/hub-client.mjs
CHANGED
|
@@ -90,7 +90,7 @@ export async function hubStatus({ force = false } = {}) {
|
|
|
90
90
|
endpoint: 'runtime',
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
|
-
return cacheStatus(evaluateHubHandshake(runtime.body));
|
|
93
|
+
return cacheStatus(evaluateHubHandshake(runtime.body, { strictProduction: true }));
|
|
94
94
|
} catch {
|
|
95
95
|
return cacheStatus(EMPTY_STATUS);
|
|
96
96
|
}
|