claude-memory-layer 1.0.54 → 1.0.55
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/index.js +417 -139
- package/dist/cli/index.js.map +4 -4
- package/dist/server/api/index.js +239 -34
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +251 -46
- package/dist/server/index.js.map +4 -4
- package/package.json +4 -2
package/dist/server/api/index.js
CHANGED
|
@@ -2243,12 +2243,12 @@ function resolveProjectStoragePath(projectOrHash) {
|
|
|
2243
2243
|
import Database from "better-sqlite3";
|
|
2244
2244
|
import * as fs4 from "fs";
|
|
2245
2245
|
import * as nodePath from "path";
|
|
2246
|
-
function createSQLiteDatabase(
|
|
2247
|
-
const dir = nodePath.dirname(
|
|
2246
|
+
function createSQLiteDatabase(path15, options) {
|
|
2247
|
+
const dir = nodePath.dirname(path15);
|
|
2248
2248
|
if (!fs4.existsSync(dir)) {
|
|
2249
2249
|
fs4.mkdirSync(dir, { recursive: true });
|
|
2250
2250
|
}
|
|
2251
|
-
const db = new Database(
|
|
2251
|
+
const db = new Database(path15, {
|
|
2252
2252
|
readonly: options?.readonly ?? false
|
|
2253
2253
|
});
|
|
2254
2254
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -2711,17 +2711,17 @@ function emptyOutboxRecoveryResult() {
|
|
|
2711
2711
|
function isRecord(value) {
|
|
2712
2712
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2713
2713
|
}
|
|
2714
|
-
function getNestedRecord(root,
|
|
2714
|
+
function getNestedRecord(root, path15) {
|
|
2715
2715
|
let cursor = root;
|
|
2716
|
-
for (const key of
|
|
2716
|
+
for (const key of path15) {
|
|
2717
2717
|
if (!isRecord(cursor)) return void 0;
|
|
2718
2718
|
cursor = cursor[key];
|
|
2719
2719
|
}
|
|
2720
2720
|
return isRecord(cursor) ? cursor : void 0;
|
|
2721
2721
|
}
|
|
2722
|
-
function getNestedString(root,
|
|
2722
|
+
function getNestedString(root, path15) {
|
|
2723
2723
|
let cursor = root;
|
|
2724
|
-
for (const key of
|
|
2724
|
+
for (const key of path15) {
|
|
2725
2725
|
if (!isRecord(cursor)) return void 0;
|
|
2726
2726
|
cursor = cursor[key];
|
|
2727
2727
|
}
|
|
@@ -6684,12 +6684,12 @@ var GraphPathService = class {
|
|
|
6684
6684
|
}
|
|
6685
6685
|
}
|
|
6686
6686
|
}
|
|
6687
|
-
const paths = Array.from(bestByTarget.entries()).map(([key,
|
|
6687
|
+
const paths = Array.from(bestByTarget.entries()).map(([key, path15]) => ({
|
|
6688
6688
|
target: graph.node(nodeFromKey(key)),
|
|
6689
|
-
hops:
|
|
6690
|
-
totalCost:
|
|
6691
|
-
scoreContribution:
|
|
6692
|
-
steps:
|
|
6689
|
+
hops: path15.hops,
|
|
6690
|
+
totalCost: path15.totalCost,
|
|
6691
|
+
scoreContribution: path15.totalCost > 0 ? 1 / path15.totalCost : 0,
|
|
6692
|
+
steps: path15.steps
|
|
6693
6693
|
})).sort((a, b) => b.scoreContribution - a.scoreContribution || a.hops - b.hops || a.target.name.localeCompare(b.target.name)).slice(0, maxResults);
|
|
6694
6694
|
return { startNodes, effectiveMaxHops, paths };
|
|
6695
6695
|
}
|
|
@@ -9462,12 +9462,12 @@ var Retriever = class {
|
|
|
9462
9462
|
direction: "both"
|
|
9463
9463
|
});
|
|
9464
9464
|
const titleByEntityId = new Map(startNodes.map((node) => [node.entityId, node.title]));
|
|
9465
|
-
for (const
|
|
9466
|
-
if (
|
|
9467
|
-
const target = await this.eventStore.getEvent(
|
|
9465
|
+
for (const path15 of expansion.paths) {
|
|
9466
|
+
if (path15.target.type !== "event") continue;
|
|
9467
|
+
const target = await this.eventStore.getEvent(path15.target.id);
|
|
9468
9468
|
if (!target) continue;
|
|
9469
|
-
const graphPath = toRetrievalGraphPathDebug(
|
|
9470
|
-
const score = graphPathScore(
|
|
9469
|
+
const graphPath = toRetrievalGraphPathDebug(path15, titleByEntityId);
|
|
9470
|
+
const score = graphPathScore(path15, opts.hopPenalty);
|
|
9471
9471
|
const existing = byId.get(target.id);
|
|
9472
9472
|
const graphPaths = mergeGraphPaths(existing?.graphPaths ?? [], [graphPath]);
|
|
9473
9473
|
const graphLane = {
|
|
@@ -9476,7 +9476,7 @@ var Retriever = class {
|
|
|
9476
9476
|
score
|
|
9477
9477
|
};
|
|
9478
9478
|
const row = {
|
|
9479
|
-
id: existing?.id ?? `graph-path-${
|
|
9479
|
+
id: existing?.id ?? `graph-path-${path15.hops}-${target.id}`,
|
|
9480
9480
|
eventId: target.id,
|
|
9481
9481
|
content: target.content,
|
|
9482
9482
|
score: Math.max(existing?.score ?? 0, score),
|
|
@@ -9816,8 +9816,8 @@ _Context:_ ${sessionContext}`;
|
|
|
9816
9816
|
}
|
|
9817
9817
|
matchesMetadataScope(metadata, expected) {
|
|
9818
9818
|
if (!metadata) return false;
|
|
9819
|
-
return Object.entries(expected).every(([
|
|
9820
|
-
const actual =
|
|
9819
|
+
return Object.entries(expected).every(([path15, value]) => {
|
|
9820
|
+
const actual = path15.split(".").reduce((acc, key) => {
|
|
9821
9821
|
if (typeof acc !== "object" || acc === null) return void 0;
|
|
9822
9822
|
return acc[key];
|
|
9823
9823
|
}, metadata);
|
|
@@ -9868,22 +9868,22 @@ function uniqueEntityStartNodes(candidates) {
|
|
|
9868
9868
|
}
|
|
9869
9869
|
return nodes;
|
|
9870
9870
|
}
|
|
9871
|
-
function toRetrievalGraphPathDebug(
|
|
9872
|
-
const firstStep =
|
|
9871
|
+
function toRetrievalGraphPathDebug(path15, titleByEntityId) {
|
|
9872
|
+
const firstStep = path15.steps[0];
|
|
9873
9873
|
const startNode = firstStep?.direction === "incoming" ? firstStep.to : firstStep?.from;
|
|
9874
9874
|
const startEntityId = startNode?.type === "entity" ? startNode.id : "";
|
|
9875
9875
|
return {
|
|
9876
9876
|
startEntityId,
|
|
9877
9877
|
startEntityTitle: titleByEntityId.get(startEntityId) ?? startNode?.name,
|
|
9878
|
-
targetId:
|
|
9879
|
-
targetType:
|
|
9880
|
-
hops:
|
|
9881
|
-
relationPath:
|
|
9878
|
+
targetId: path15.target.id,
|
|
9879
|
+
targetType: path15.target.type,
|
|
9880
|
+
hops: path15.hops,
|
|
9881
|
+
relationPath: path15.steps.map((step) => step.relationType)
|
|
9882
9882
|
};
|
|
9883
9883
|
}
|
|
9884
|
-
function graphPathScore(
|
|
9885
|
-
const base = Math.min(0.95, Math.max(0,
|
|
9886
|
-
return Math.max(0.05, base - hopPenalty * Math.max(0,
|
|
9884
|
+
function graphPathScore(path15, hopPenalty) {
|
|
9885
|
+
const base = Math.min(0.95, Math.max(0, path15.scoreContribution));
|
|
9886
|
+
return Math.max(0.05, base - hopPenalty * Math.max(0, path15.hops - 1));
|
|
9887
9887
|
}
|
|
9888
9888
|
function clampGraphHops(maxHops) {
|
|
9889
9889
|
if (!Number.isFinite(maxHops)) return 2;
|
|
@@ -9891,14 +9891,14 @@ function clampGraphHops(maxHops) {
|
|
|
9891
9891
|
}
|
|
9892
9892
|
function mergeGraphPaths(existing, incoming) {
|
|
9893
9893
|
const byKey = /* @__PURE__ */ new Map();
|
|
9894
|
-
for (const
|
|
9895
|
-
const key = [
|
|
9896
|
-
if (!byKey.has(key)) byKey.set(key,
|
|
9894
|
+
for (const path15 of [...existing, ...incoming]) {
|
|
9895
|
+
const key = [path15.startEntityId, path15.targetType, path15.targetId, path15.hops, ...path15.relationPath].join("\0");
|
|
9896
|
+
if (!byKey.has(key)) byKey.set(key, path15);
|
|
9897
9897
|
}
|
|
9898
9898
|
return [...byKey.values()].sort((a, b) => a.hops - b.hops || compareStable(graphPathSignature(a), graphPathSignature(b))).slice(0, 3);
|
|
9899
9899
|
}
|
|
9900
|
-
function graphPathSignature(
|
|
9901
|
-
return [
|
|
9900
|
+
function graphPathSignature(path15) {
|
|
9901
|
+
return [path15.startEntityId, path15.targetType, path15.targetId, path15.hops, ...path15.relationPath].join("|");
|
|
9902
9902
|
}
|
|
9903
9903
|
function compareStable(a, b) {
|
|
9904
9904
|
if (a < b) return -1;
|
|
@@ -15561,6 +15561,150 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
15561
15561
|
// src/apps/server/api/health.ts
|
|
15562
15562
|
import { Hono as Hono9 } from "hono";
|
|
15563
15563
|
import { spawnSync } from "child_process";
|
|
15564
|
+
import * as fs12 from "fs";
|
|
15565
|
+
import * as path14 from "path";
|
|
15566
|
+
|
|
15567
|
+
// src/core/productivity-health-report.ts
|
|
15568
|
+
var PRODUCTIVITY_HEALTH_SCHEMA_VERSION = "agent-productivity-health-v1";
|
|
15569
|
+
var PRODUCTIVITY_HEALTH_PROFILES = ["coder", "reviewer", "pm", "support", "researcher", "team"];
|
|
15570
|
+
var PRODUCTIVITY_HEALTH_MODES = ["observe", "preview", "enforce"];
|
|
15571
|
+
function parseProductivityHealthProfile(value) {
|
|
15572
|
+
const normalized = typeof value === "string" && value.trim().length > 0 ? value.trim() : "coder";
|
|
15573
|
+
if (PRODUCTIVITY_HEALTH_PROFILES.includes(normalized)) {
|
|
15574
|
+
return normalized;
|
|
15575
|
+
}
|
|
15576
|
+
throw new Error(`Invalid --profile: expected one of ${PRODUCTIVITY_HEALTH_PROFILES.join(", ")}`);
|
|
15577
|
+
}
|
|
15578
|
+
function parseProductivityHealthMode(value) {
|
|
15579
|
+
const normalized = typeof value === "string" && value.trim().length > 0 ? value.trim() : "preview";
|
|
15580
|
+
if (PRODUCTIVITY_HEALTH_MODES.includes(normalized)) {
|
|
15581
|
+
return normalized;
|
|
15582
|
+
}
|
|
15583
|
+
throw new Error(`Invalid --mode: expected one of ${PRODUCTIVITY_HEALTH_MODES.join(", ")}`);
|
|
15584
|
+
}
|
|
15585
|
+
function buildProductivityHealthReport(input) {
|
|
15586
|
+
const profile = input.profile ?? "coder";
|
|
15587
|
+
const mode = input.mode ?? "preview";
|
|
15588
|
+
const embedding = normalizeQueue(input.outbox.embedding);
|
|
15589
|
+
const vector = normalizeQueue(input.outbox.vector);
|
|
15590
|
+
const totals = sumQueues(embedding, vector);
|
|
15591
|
+
const storage = {
|
|
15592
|
+
totalEvents: numberOrZero(input.stats.totalEvents),
|
|
15593
|
+
vectorCount: numberOrZero(input.stats.vectorCount),
|
|
15594
|
+
levelStats: normalizeLevelStats(input.stats.levelStats)
|
|
15595
|
+
};
|
|
15596
|
+
const warningReasons = [];
|
|
15597
|
+
const projectGate = buildProjectScopeGate(input.project);
|
|
15598
|
+
const outboxGate = buildOutboxGate(totals);
|
|
15599
|
+
const densityGate = buildMemoryDensityGate(storage.totalEvents);
|
|
15600
|
+
const riskGates = [projectGate, outboxGate, densityGate];
|
|
15601
|
+
if (projectGate.status === "warn") warningReasons.push("project_scope_unknown");
|
|
15602
|
+
if (outboxGate.status === "warn") warningReasons.push("outbox_requires_attention");
|
|
15603
|
+
if (densityGate.status === "warn") warningReasons.push("memory_density_low");
|
|
15604
|
+
const status = riskGates.some((gate) => gate.status === "warn") ? "needs-attention" : "ok";
|
|
15605
|
+
return {
|
|
15606
|
+
schemaVersion: PRODUCTIVITY_HEALTH_SCHEMA_VERSION,
|
|
15607
|
+
generatedAt: (input.generatedAt ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
15608
|
+
status,
|
|
15609
|
+
profile,
|
|
15610
|
+
mode,
|
|
15611
|
+
project: input.project,
|
|
15612
|
+
summary: { warningReasons },
|
|
15613
|
+
signals: {
|
|
15614
|
+
storage,
|
|
15615
|
+
outbox: { embedding, vector, totals }
|
|
15616
|
+
},
|
|
15617
|
+
riskGates,
|
|
15618
|
+
nextBestAction: selectNextBestAction({ projectGate, outboxGate, densityGate })
|
|
15619
|
+
};
|
|
15620
|
+
}
|
|
15621
|
+
function buildProjectScopeGate(project) {
|
|
15622
|
+
if (project.scope === "project") {
|
|
15623
|
+
return { id: "project-scope-known", severity: "blocker", status: "pass" };
|
|
15624
|
+
}
|
|
15625
|
+
return {
|
|
15626
|
+
id: "project-scope-known",
|
|
15627
|
+
severity: "blocker",
|
|
15628
|
+
status: "warn",
|
|
15629
|
+
message: "No project parameter supplied; report uses global memory scope."
|
|
15630
|
+
};
|
|
15631
|
+
}
|
|
15632
|
+
function buildOutboxGate(totals) {
|
|
15633
|
+
if (totals.failed > 0 || totals.stuckProcessing > 0) {
|
|
15634
|
+
return {
|
|
15635
|
+
id: "outbox-healthy",
|
|
15636
|
+
severity: "warning",
|
|
15637
|
+
status: "warn",
|
|
15638
|
+
message: "Outbox has failed or stuck processing items; run recovery before trusting retrieval freshness."
|
|
15639
|
+
};
|
|
15640
|
+
}
|
|
15641
|
+
return { id: "outbox-healthy", severity: "warning", status: "pass" };
|
|
15642
|
+
}
|
|
15643
|
+
function buildMemoryDensityGate(totalEvents) {
|
|
15644
|
+
if (totalEvents <= 0) {
|
|
15645
|
+
return {
|
|
15646
|
+
id: "memory-density",
|
|
15647
|
+
severity: "warning",
|
|
15648
|
+
status: "warn",
|
|
15649
|
+
message: "No memories are available for this scope yet."
|
|
15650
|
+
};
|
|
15651
|
+
}
|
|
15652
|
+
return { id: "memory-density", severity: "warning", status: "pass" };
|
|
15653
|
+
}
|
|
15654
|
+
function selectNextBestAction(input) {
|
|
15655
|
+
if (input.outboxGate.status === "warn") {
|
|
15656
|
+
return "Run claude-memory-layer process --dry-run-recovery, then process pending embeddings.";
|
|
15657
|
+
}
|
|
15658
|
+
if (input.densityGate.status === "warn") {
|
|
15659
|
+
return "Import or capture project context before relying on productivity memory guidance.";
|
|
15660
|
+
}
|
|
15661
|
+
if (input.projectGate.status === "warn") {
|
|
15662
|
+
return "Provide a project path or project hash before relying on project-scoped productivity guidance.";
|
|
15663
|
+
}
|
|
15664
|
+
return "No immediate maintenance action required.";
|
|
15665
|
+
}
|
|
15666
|
+
function normalizeLevelStats(levelStats) {
|
|
15667
|
+
if (!Array.isArray(levelStats)) return [];
|
|
15668
|
+
return levelStats.filter((entry) => typeof entry.level === "string" && entry.level.trim().length > 0).map((entry) => ({ level: entry.level.trim().slice(0, 20), count: numberOrZero(entry.count) }));
|
|
15669
|
+
}
|
|
15670
|
+
function normalizeQueue(queue) {
|
|
15671
|
+
return {
|
|
15672
|
+
pending: numberOrZero(queue.pending),
|
|
15673
|
+
processing: numberOrZero(queue.processing),
|
|
15674
|
+
failed: numberOrZero(queue.failed),
|
|
15675
|
+
retryableFailed: numberOrZero(queue.retryableFailed),
|
|
15676
|
+
quarantinedFailed: numberOrZero(queue.quarantinedFailed),
|
|
15677
|
+
total: numberOrZero(queue.total),
|
|
15678
|
+
stuckProcessing: numberOrZero(queue.stuckProcessing),
|
|
15679
|
+
oldestProcessingAgeMs: normalizeNullableAge(queue.oldestProcessingAgeMs)
|
|
15680
|
+
};
|
|
15681
|
+
}
|
|
15682
|
+
function sumQueues(a, b) {
|
|
15683
|
+
return {
|
|
15684
|
+
pending: a.pending + b.pending,
|
|
15685
|
+
processing: a.processing + b.processing,
|
|
15686
|
+
failed: a.failed + b.failed,
|
|
15687
|
+
retryableFailed: (a.retryableFailed ?? 0) + (b.retryableFailed ?? 0),
|
|
15688
|
+
quarantinedFailed: (a.quarantinedFailed ?? 0) + (b.quarantinedFailed ?? 0),
|
|
15689
|
+
total: a.total + b.total,
|
|
15690
|
+
stuckProcessing: a.stuckProcessing + b.stuckProcessing,
|
|
15691
|
+
oldestProcessingAgeMs: maxNullable(a.oldestProcessingAgeMs, b.oldestProcessingAgeMs)
|
|
15692
|
+
};
|
|
15693
|
+
}
|
|
15694
|
+
function normalizeNullableAge(value) {
|
|
15695
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
|
|
15696
|
+
}
|
|
15697
|
+
function maxNullable(a, b) {
|
|
15698
|
+
if (a === null) return b;
|
|
15699
|
+
if (b === null) return a;
|
|
15700
|
+
return Math.max(a, b);
|
|
15701
|
+
}
|
|
15702
|
+
function numberOrZero(value) {
|
|
15703
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
15704
|
+
return Math.floor(value);
|
|
15705
|
+
}
|
|
15706
|
+
|
|
15707
|
+
// src/apps/server/api/health.ts
|
|
15564
15708
|
var healthRouter = new Hono9();
|
|
15565
15709
|
function hasEnvSignal(name) {
|
|
15566
15710
|
const value = process.env[name];
|
|
@@ -15601,6 +15745,33 @@ function aggregateOutbox(outbox) {
|
|
|
15601
15745
|
const stuckProcessing = (outbox.embedding?.stuckProcessing || 0) + (outbox.vector?.stuckProcessing || 0);
|
|
15602
15746
|
return { pending, processing, failed, retryableFailed, quarantinedFailed, stuckProcessing };
|
|
15603
15747
|
}
|
|
15748
|
+
function resolveProductivityProjectIdentity(project) {
|
|
15749
|
+
if (!project || project.trim().length === 0) return { scope: "global", id: "global" };
|
|
15750
|
+
const normalized = project.trim();
|
|
15751
|
+
return { scope: "project", id: /^[a-f0-9]{8}$/.test(normalized) ? normalized : hashProjectPath(normalized) };
|
|
15752
|
+
}
|
|
15753
|
+
function explicitProjectStoreExists(project) {
|
|
15754
|
+
if (!project || project.trim().length === 0) return true;
|
|
15755
|
+
return fs12.existsSync(path14.join(resolveProjectStoragePath(project.trim()), "events.sqlite"));
|
|
15756
|
+
}
|
|
15757
|
+
function emptyProductivityStats() {
|
|
15758
|
+
return { totalEvents: 0, vectorCount: 0, levelStats: [] };
|
|
15759
|
+
}
|
|
15760
|
+
function emptyProductivityQueue() {
|
|
15761
|
+
return {
|
|
15762
|
+
pending: 0,
|
|
15763
|
+
processing: 0,
|
|
15764
|
+
failed: 0,
|
|
15765
|
+
retryableFailed: 0,
|
|
15766
|
+
quarantinedFailed: 0,
|
|
15767
|
+
total: 0,
|
|
15768
|
+
stuckProcessing: 0,
|
|
15769
|
+
oldestProcessingAgeMs: null
|
|
15770
|
+
};
|
|
15771
|
+
}
|
|
15772
|
+
function emptyProductivityOutbox() {
|
|
15773
|
+
return { embedding: emptyProductivityQueue(), vector: emptyProductivityQueue() };
|
|
15774
|
+
}
|
|
15604
15775
|
healthRouter.get("/setup", async (c) => {
|
|
15605
15776
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
15606
15777
|
try {
|
|
@@ -15657,6 +15828,40 @@ healthRouter.get("/setup", async (c) => {
|
|
|
15657
15828
|
await memoryService.shutdown();
|
|
15658
15829
|
}
|
|
15659
15830
|
});
|
|
15831
|
+
healthRouter.get("/productivity", async (c) => {
|
|
15832
|
+
let memoryService;
|
|
15833
|
+
try {
|
|
15834
|
+
const profile = parseProductivityHealthProfile(c.req.query("profile"));
|
|
15835
|
+
const mode = parseProductivityHealthMode(c.req.query("mode"));
|
|
15836
|
+
const projectQuery = c.req.query("project") || c.req.query("projectId");
|
|
15837
|
+
const project = resolveProductivityProjectIdentity(projectQuery);
|
|
15838
|
+
if (!explicitProjectStoreExists(projectQuery)) {
|
|
15839
|
+
return c.json(buildProductivityHealthReport({
|
|
15840
|
+
stats: emptyProductivityStats(),
|
|
15841
|
+
outbox: emptyProductivityOutbox(),
|
|
15842
|
+
project,
|
|
15843
|
+
profile,
|
|
15844
|
+
mode
|
|
15845
|
+
}));
|
|
15846
|
+
}
|
|
15847
|
+
memoryService = getLightweightServiceFromQuery(c);
|
|
15848
|
+
await memoryService.initialize();
|
|
15849
|
+
const [stats, outbox] = await Promise.all([
|
|
15850
|
+
memoryService.getStats(),
|
|
15851
|
+
memoryService.getOutboxStats()
|
|
15852
|
+
]);
|
|
15853
|
+
return c.json(buildProductivityHealthReport({ stats, outbox, project, profile, mode }));
|
|
15854
|
+
} catch (error) {
|
|
15855
|
+
const status = error instanceof Error && error.message.startsWith("Invalid --") ? 400 : 500;
|
|
15856
|
+
return c.json({
|
|
15857
|
+
status: "error",
|
|
15858
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15859
|
+
error: status === 400 ? "Invalid productivity health option" : "Productivity health check failed"
|
|
15860
|
+
}, status);
|
|
15861
|
+
} finally {
|
|
15862
|
+
if (memoryService) await memoryService.shutdown();
|
|
15863
|
+
}
|
|
15864
|
+
});
|
|
15660
15865
|
healthRouter.get("/", async (c) => {
|
|
15661
15866
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
15662
15867
|
try {
|