@rubytech/create-maxy 1.0.881 → 1.0.884
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/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/index.js +45 -0
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/index.ts +47 -0
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +57 -9
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -1
- package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts +26 -0
- package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/conversation-provenance.js +81 -0
- package/payload/platform/lib/graph-write/dist/conversation-provenance.js.map +1 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts +38 -16
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +75 -35
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +59 -9
- package/payload/platform/lib/graph-write/src/conversation-provenance.ts +140 -0
- package/payload/platform/lib/graph-write/src/index.ts +76 -35
- package/payload/platform/lib/mcp-eager/dist/index.d.ts +61 -0
- package/payload/platform/lib/mcp-eager/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/mcp-eager/dist/index.js +49 -0
- package/payload/platform/lib/mcp-eager/dist/index.js.map +1 -0
- package/payload/platform/lib/mcp-eager/src/index.ts +78 -0
- package/payload/platform/lib/mcp-eager/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/mcp/dist/__tests__/plugin-read-skill-resolution.test.js +26 -2
- package/payload/platform/plugins/admin/mcp/dist/__tests__/plugin-read-skill-resolution.test.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +36 -33
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts +5 -1
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js +24 -8
- package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js.map +1 -1
- package/payload/platform/plugins/contacts/PLUGIN.md +8 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js +10 -9
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +17 -2
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
- package/payload/platform/plugins/docs/references/internals.md +15 -2
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +10 -9
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +5 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js +10 -9
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +18 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +9 -8
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/index.js +15 -14
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/index.js +4 -3
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/index.js +9 -8
- package/payload/platform/plugins/workflows/mcp/dist/index.js.map +1 -1
- package/payload/platform/scripts/__tests__/logs-read-prefix.sh +341 -0
- package/payload/platform/scripts/logs-read.sh +108 -41
- package/payload/platform/scripts/logs-read.test.sh +6 -2
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/premium-plugins/real-agency/BUNDLE.md +1 -1
- package/payload/server/chunk-5PQU2HW2.js +11672 -0
- package/payload/server/chunk-ECAQVMRA.js +759 -0
- package/payload/server/chunk-K7S5T4VG.js +11534 -0
- package/payload/server/cloudflare-task-tracker-JNZXLW32.js +22 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/server.js +38 -6
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSession
|
|
3
|
+
} from "./chunk-DOIAYD3J.js";
|
|
4
|
+
import {
|
|
5
|
+
__commonJS,
|
|
6
|
+
__toESM
|
|
7
|
+
} from "./chunk-JSBRDJBE.js";
|
|
8
|
+
|
|
9
|
+
// ../lib/graph-write/dist/audit.js
|
|
10
|
+
var require_audit = __commonJS({
|
|
11
|
+
"../lib/graph-write/dist/audit.js"(exports) {
|
|
12
|
+
"use strict";
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.auditCypherWrite = auditCypherWrite;
|
|
15
|
+
exports.formatAuditLine = formatAuditLine;
|
|
16
|
+
var EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
|
|
17
|
+
var CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
|
|
18
|
+
var PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
|
|
19
|
+
function stripStringLiterals(cypher) {
|
|
20
|
+
return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
|
|
21
|
+
}
|
|
22
|
+
function extractEdgeTypes(cleaned) {
|
|
23
|
+
const out = /* @__PURE__ */ new Set();
|
|
24
|
+
for (const m of cleaned.matchAll(EDGE_PATTERN)) {
|
|
25
|
+
for (const t of m[1].split("|")) {
|
|
26
|
+
const clean = t.trim();
|
|
27
|
+
if (clean)
|
|
28
|
+
out.add(clean);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function countCreateOrMergeNodes(cleaned) {
|
|
34
|
+
const matches = cleaned.match(CREATE_OR_MERGE_NODE);
|
|
35
|
+
return matches ? matches.length : 0;
|
|
36
|
+
}
|
|
37
|
+
function countProvenanceStamps(cleaned) {
|
|
38
|
+
const matches = cleaned.match(PROVENANCE_TOKEN);
|
|
39
|
+
return matches ? matches.length : 0;
|
|
40
|
+
}
|
|
41
|
+
function auditCypherWrite(input) {
|
|
42
|
+
const warnings = [];
|
|
43
|
+
const cleaned = stripStringLiterals(input.cypher);
|
|
44
|
+
const referencedTypes = extractEdgeTypes(cleaned);
|
|
45
|
+
for (const t of referencedTypes) {
|
|
46
|
+
if (!input.schema.relationshipTypes.has(t)) {
|
|
47
|
+
warnings.push({ kind: "unknown-type-warning", type: t });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (input.nodesCreated > 0) {
|
|
51
|
+
const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
|
|
52
|
+
if (createOrMergeNodes > 0) {
|
|
53
|
+
const stamps = countProvenanceStamps(cleaned);
|
|
54
|
+
if (stamps < createOrMergeNodes) {
|
|
55
|
+
warnings.push({
|
|
56
|
+
kind: "missing-provenance-warning",
|
|
57
|
+
created: createOrMergeNodes,
|
|
58
|
+
stamped: stamps
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (input.orphanIds.length > 0) {
|
|
64
|
+
warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
|
|
65
|
+
}
|
|
66
|
+
return warnings;
|
|
67
|
+
}
|
|
68
|
+
function formatAuditLine(line) {
|
|
69
|
+
const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
|
|
70
|
+
switch (line.kind) {
|
|
71
|
+
case "accepted":
|
|
72
|
+
return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
|
|
73
|
+
case "orphan-warning":
|
|
74
|
+
return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
|
|
75
|
+
case "unknown-type-warning":
|
|
76
|
+
return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
|
|
77
|
+
case "missing-provenance-warning":
|
|
78
|
+
return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ../lib/graph-write/dist/conversation-provenance.js
|
|
85
|
+
var require_conversation_provenance = __commonJS({
|
|
86
|
+
"../lib/graph-write/dist/conversation-provenance.js"(exports) {
|
|
87
|
+
"use strict";
|
|
88
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
89
|
+
exports.injectConversationProvenance = injectConversationProvenance;
|
|
90
|
+
var index_js_1 = require_dist();
|
|
91
|
+
async function injectConversationProvenance(params) {
|
|
92
|
+
const { session, relationships, accountId, writeLabels, conversationNodeId, logNamespace, tool } = params;
|
|
93
|
+
const original = [...relationships];
|
|
94
|
+
if (!writeLabels.some((l) => index_js_1.ACTION_PROVENANCE_LABELS.has(l)))
|
|
95
|
+
return original;
|
|
96
|
+
if (!conversationNodeId)
|
|
97
|
+
return original;
|
|
98
|
+
if (hasInboundProducedEdge(original))
|
|
99
|
+
return original;
|
|
100
|
+
let lookup;
|
|
101
|
+
try {
|
|
102
|
+
lookup = await session.run(`MATCH (n) WHERE elementId(n) = $id RETURN labels(n) AS labels, n.accountId AS accountId LIMIT 1`, { id: conversationNodeId });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=driver-error message=${err instanceof Error ? err.message : String(err)} conversationNodeId=${conversationNodeId}
|
|
105
|
+
`);
|
|
106
|
+
return original;
|
|
107
|
+
}
|
|
108
|
+
if (lookup.records.length === 0) {
|
|
109
|
+
process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=node-not-found conversationNodeId=${conversationNodeId}
|
|
110
|
+
`);
|
|
111
|
+
return original;
|
|
112
|
+
}
|
|
113
|
+
const labels = lookup.records[0].get("labels");
|
|
114
|
+
const sourceAccountId = lookup.records[0].get("accountId");
|
|
115
|
+
const sourceLabel = labels.find((l) => index_js_1.PROVENANCE_SOURCE_LABELS.has(l));
|
|
116
|
+
if (!sourceLabel) {
|
|
117
|
+
process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=wrong-source-label labels=${labels.join(",")} conversationNodeId=${conversationNodeId}
|
|
118
|
+
`);
|
|
119
|
+
return original;
|
|
120
|
+
}
|
|
121
|
+
if (sourceAccountId !== accountId) {
|
|
122
|
+
process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=account-mismatch sourceAccountId=${sourceAccountId.slice(0, 8)} writeAccountId=${accountId.slice(0, 8)} conversationNodeId=${conversationNodeId}
|
|
123
|
+
`);
|
|
124
|
+
return original;
|
|
125
|
+
}
|
|
126
|
+
process.stderr.write(`[${logNamespace}] [provenance-inject] tool=${tool} from=${sourceLabel}:${conversationNodeId}
|
|
127
|
+
`);
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
type: "PRODUCED",
|
|
131
|
+
direction: "incoming",
|
|
132
|
+
targetNodeId: conversationNodeId
|
|
133
|
+
},
|
|
134
|
+
...original
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
function hasInboundProducedEdge(relationships) {
|
|
138
|
+
return relationships.some((r) => r.type === "PRODUCED" && r.direction === "incoming");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ../lib/graph-write/dist/index.js
|
|
144
|
+
var require_dist = __commonJS({
|
|
145
|
+
"../lib/graph-write/dist/index.js"(exports) {
|
|
146
|
+
"use strict";
|
|
147
|
+
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
148
|
+
if (k2 === void 0) k2 = k;
|
|
149
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
150
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
151
|
+
desc = { enumerable: true, get: function() {
|
|
152
|
+
return m[k];
|
|
153
|
+
} };
|
|
154
|
+
}
|
|
155
|
+
Object.defineProperty(o, k2, desc);
|
|
156
|
+
}) : (function(o, m, k, k2) {
|
|
157
|
+
if (k2 === void 0) k2 = k;
|
|
158
|
+
o[k2] = m[k];
|
|
159
|
+
}));
|
|
160
|
+
var __exportStar = exports && exports.__exportStar || function(m, exports2) {
|
|
161
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
162
|
+
};
|
|
163
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
164
|
+
exports.PROVENANCE_SOURCE_LABELS = exports.ACTION_PROVENANCE_LABELS = void 0;
|
|
165
|
+
exports.stampCreatedBy = stampCreatedBy;
|
|
166
|
+
exports.writeNodeWithEdges = writeNodeWithEdges2;
|
|
167
|
+
__exportStar(require_audit(), exports);
|
|
168
|
+
__exportStar(require_conversation_provenance(), exports);
|
|
169
|
+
exports.ACTION_PROVENANCE_LABELS = /* @__PURE__ */ new Set([
|
|
170
|
+
"Person",
|
|
171
|
+
"UserProfile",
|
|
172
|
+
"AdminUser",
|
|
173
|
+
"Organization",
|
|
174
|
+
"LocalBusiness",
|
|
175
|
+
"CloudflareTunnel",
|
|
176
|
+
"CloudflareHostname"
|
|
177
|
+
]);
|
|
178
|
+
exports.PROVENANCE_SOURCE_LABELS = /* @__PURE__ */ new Set([
|
|
179
|
+
"Task",
|
|
180
|
+
"Conversation",
|
|
181
|
+
"Message"
|
|
182
|
+
]);
|
|
183
|
+
function requiresActionProvenance(labels) {
|
|
184
|
+
for (const label of labels) {
|
|
185
|
+
if (exports.ACTION_PROVENANCE_LABELS.has(label))
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
function findProvenanceCandidates(relationships) {
|
|
191
|
+
return relationships.filter((r) => r.type === "PRODUCED" && r.direction === "incoming");
|
|
192
|
+
}
|
|
193
|
+
function stampCreatedBy(props, createdBy) {
|
|
194
|
+
return {
|
|
195
|
+
...props,
|
|
196
|
+
createdByAgent: createdBy.agent ?? "unknown",
|
|
197
|
+
createdBySession: createdBy.session ?? "unknown",
|
|
198
|
+
createdByTool: createdBy.tool ?? null,
|
|
199
|
+
createdBySource: createdBy.source ?? null
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async function writeNodeWithEdges2(params) {
|
|
203
|
+
const { session, labels, props, relationships, createdBy } = params;
|
|
204
|
+
const agentLabel = createdBy.agent ?? createdBy.source ?? "unknown";
|
|
205
|
+
const labelCsv = labels.join(",");
|
|
206
|
+
const isSystemBootstrap = (createdBy.agent ?? "") === "system";
|
|
207
|
+
if (!isSystemBootstrap) {
|
|
208
|
+
const accountId = props.accountId;
|
|
209
|
+
const expectedAccountId = params.expectedAccountId ?? process.env.ACCOUNT_ID;
|
|
210
|
+
if (typeof accountId !== "string" || !expectedAccountId || accountId !== expectedAccountId) {
|
|
211
|
+
const slice = typeof accountId === "string" ? accountId.slice(0, 8) : "missing";
|
|
212
|
+
process.stderr.write(`[graph-write] reject reason=invalid-account-id accountId=${slice} writer=${agentLabel}
|
|
213
|
+
`);
|
|
214
|
+
throw new Error(`Write doctrine violated: invalid-account-id (${slice}) \u2014 accountId must equal ACCOUNT_ID set by the spawning process. See .docs/neo4j.md "Account isolation invariant".`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const reviewDigestActionTool = typeof props.actionTool === "string" && props.actionTool === "review-digest-compose";
|
|
218
|
+
if (labels.includes("ReviewAlert") || reviewDigestActionTool) {
|
|
219
|
+
const actionToolField = reviewDigestActionTool ? "review-digest-compose" : "n/a";
|
|
220
|
+
process.stderr.write(`[graph-write] reject reason=removed-feature labels=${labelCsv} actionTool=${actionToolField} agent=${agentLabel}
|
|
221
|
+
`);
|
|
222
|
+
throw new Error("Write doctrine violated: review-detector feature removed (Task 884) \u2014 `:ReviewAlert` and `:Event {actionTool:'review-digest-compose'}` writes are not allowed.");
|
|
223
|
+
}
|
|
224
|
+
if (!relationships || relationships.length < 1) {
|
|
225
|
+
process.stderr.write(`[graph-write] reject reason=zero-relationships labels=${labelCsv} agent=${agentLabel}
|
|
226
|
+
`);
|
|
227
|
+
throw new Error("Write doctrine violated: a node must be created with at least one relationship. See .docs/neo4j.md (Write doctrine).");
|
|
228
|
+
}
|
|
229
|
+
const labelStr = labels.map((l) => `\`${l.replace(/`/g, "")}\``).join(":");
|
|
230
|
+
const nodeProps = stampCreatedBy(props, createdBy);
|
|
231
|
+
return await session.executeWrite(async (tx) => {
|
|
232
|
+
const targetIds = relationships.map((r) => r.targetNodeId);
|
|
233
|
+
const check = await tx.run(`UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN elementId(t) AS id, labels(t) AS labels`, { ids: targetIds });
|
|
234
|
+
const labelsByTarget = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const rec of check.records) {
|
|
236
|
+
labelsByTarget.set(rec.get("id"), rec.get("labels"));
|
|
237
|
+
}
|
|
238
|
+
const found = labelsByTarget.size;
|
|
239
|
+
const uniqueRequested = new Set(targetIds).size;
|
|
240
|
+
if (found !== uniqueRequested) {
|
|
241
|
+
process.stderr.write(`[graph-write] reject reason=unresolved-target labels=${labelCsv} agent=${agentLabel} requested=${uniqueRequested} found=${found}
|
|
242
|
+
`);
|
|
243
|
+
throw new Error(`Write doctrine violated: ${uniqueRequested - found} of ${uniqueRequested} relationship target(s) did not resolve (elementId mismatch). No node created.`);
|
|
244
|
+
}
|
|
245
|
+
let provenanceSourceId = null;
|
|
246
|
+
let provenanceSourceLabel = null;
|
|
247
|
+
if (requiresActionProvenance(labels) && (createdBy.agent ?? "") !== "system") {
|
|
248
|
+
const candidates = findProvenanceCandidates(relationships);
|
|
249
|
+
const matched = candidates.map((r) => {
|
|
250
|
+
const lbls = labelsByTarget.get(r.targetNodeId);
|
|
251
|
+
if (!Array.isArray(lbls))
|
|
252
|
+
return null;
|
|
253
|
+
const sourceLabel = lbls.find((l) => exports.PROVENANCE_SOURCE_LABELS.has(l));
|
|
254
|
+
return sourceLabel ? { rel: r, sourceLabel } : null;
|
|
255
|
+
}).filter((m) => m !== null);
|
|
256
|
+
if (matched.length === 0) {
|
|
257
|
+
process.stderr.write(`[graph-write] reject reason=missing-provenance labels=${labelCsv} agent=${agentLabel}
|
|
258
|
+
`);
|
|
259
|
+
throw new Error(`missing-provenance: write to ${labelCsv} requires an inbound :PRODUCED edge from a :Task, :Conversation, or :Message (createdBy.agent='${agentLabel}'). Either pass producedByTaskId on the write (autonomous workflows: call task-create at the start of the flow and thread the returned taskId), or rely on the MCP wrapper's CONVERSATION_NODE_ID env-stamp injection (direct admin asks).`);
|
|
260
|
+
}
|
|
261
|
+
provenanceSourceId = matched[0].rel.targetNodeId;
|
|
262
|
+
provenanceSourceLabel = matched[0].sourceLabel;
|
|
263
|
+
}
|
|
264
|
+
let nodeRes;
|
|
265
|
+
try {
|
|
266
|
+
nodeRes = await tx.run(`CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`, { props: nodeProps });
|
|
267
|
+
} catch (err) {
|
|
268
|
+
const code = err?.code ?? "";
|
|
269
|
+
if (code === "Neo.ClientError.Schema.ConstraintValidationFailed" && labels.includes("UserProfile")) {
|
|
270
|
+
const accountIdProp = nodeProps.accountId;
|
|
271
|
+
const userIdProp = nodeProps.userId;
|
|
272
|
+
const acctSlice = typeof accountIdProp === "string" ? accountIdProp.slice(0, 8) : "unknown";
|
|
273
|
+
const userSlice = typeof userIdProp === "string" ? userIdProp.slice(0, 8) : "unknown";
|
|
274
|
+
process.stderr.write(`[graph-write] reject reason=user-profile-uniqueness-violation accountId=${acctSlice} userId=${userSlice} writer=${agentLabel}
|
|
275
|
+
`);
|
|
276
|
+
}
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
const nodeId = nodeRes.records[0].get("nodeId");
|
|
280
|
+
const nodeLabels = nodeRes.records[0].get("nodeLabels");
|
|
281
|
+
let edgesCreated = 0;
|
|
282
|
+
for (const rel of relationships) {
|
|
283
|
+
const type = rel.type.replace(/`/g, "");
|
|
284
|
+
const q = rel.direction === "outgoing" ? `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (a)-[:\`${type}\`]->(b)` : `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (b)-[:\`${type}\`]->(a)`;
|
|
285
|
+
const r = await tx.run(q, { from: nodeId, to: rel.targetNodeId });
|
|
286
|
+
const created = r.summary.counters.updates().relationshipsCreated;
|
|
287
|
+
if (created === 0) {
|
|
288
|
+
process.stderr.write(`[graph-write] reject reason=unresolved-target-on-create labels=${labelCsv} agent=${agentLabel} relType=${rel.type} targetId=${rel.targetNodeId}
|
|
289
|
+
`);
|
|
290
|
+
throw new Error(`Write doctrine violated: relationship CREATE to target ${rel.targetNodeId} produced 0 edges (target likely deleted concurrently after pre-check). Transaction rolled back.`);
|
|
291
|
+
}
|
|
292
|
+
edgesCreated += created;
|
|
293
|
+
}
|
|
294
|
+
if (edgesCreated !== relationships.length) {
|
|
295
|
+
process.stderr.write(`[graph-write] reject reason=edge-count-mismatch labels=${labelCsv} agent=${agentLabel} requested=${relationships.length} created=${edgesCreated}
|
|
296
|
+
`);
|
|
297
|
+
throw new Error(`Write doctrine violated: expected ${relationships.length} edges, created ${edgesCreated}. Transaction rolled back.`);
|
|
298
|
+
}
|
|
299
|
+
process.stderr.write(`[graph-write] accepted labels=${labelCsv} edges=${edgesCreated} createdByAgent=${createdBy.agent ?? "unknown"} createdByTool=${createdBy.tool ?? createdBy.source ?? "unknown"} producedBy=${provenanceSourceLabel ?? "none"}:${provenanceSourceId ?? "none"}
|
|
300
|
+
`);
|
|
301
|
+
return { nodeId, labels: nodeLabels, edgesCreated };
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ../lib/task-secrets/dist/index.js
|
|
308
|
+
var require_dist2 = __commonJS({
|
|
309
|
+
"../lib/task-secrets/dist/index.js"(exports) {
|
|
310
|
+
"use strict";
|
|
311
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
312
|
+
exports.redactSecrets = redactSecrets2;
|
|
313
|
+
function redactSecrets2(payload, schema) {
|
|
314
|
+
if (!payload) {
|
|
315
|
+
return { redacted: {}, droppedFields: 0, droppedFieldNames: [] };
|
|
316
|
+
}
|
|
317
|
+
const secrets = new Set(schema?.secretFields ?? []);
|
|
318
|
+
const redacted = {};
|
|
319
|
+
const dropped = [];
|
|
320
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
321
|
+
if (secrets.has(key)) {
|
|
322
|
+
dropped.push(key);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
redacted[key] = value;
|
|
326
|
+
}
|
|
327
|
+
dropped.sort();
|
|
328
|
+
return { redacted, droppedFields: dropped.length, droppedFieldNames: dropped };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// app/lib/cloudflare-task-tracker.ts
|
|
334
|
+
import { readFileSync, existsSync } from "fs";
|
|
335
|
+
import { randomUUID } from "crypto";
|
|
336
|
+
var import_dist = __toESM(require_dist(), 1);
|
|
337
|
+
var import_dist2 = __toESM(require_dist2(), 1);
|
|
338
|
+
var CREATED_BY_AGENT = "cloudflare-setup-endpoint";
|
|
339
|
+
var TASK_KIND = "cloudflare-tunnel-login";
|
|
340
|
+
async function openCloudflareTask(params) {
|
|
341
|
+
const { accountId, conversationId, inputsProvided, inputs, inputSchema, messageId } = params;
|
|
342
|
+
const taskId = randomUUID();
|
|
343
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
344
|
+
const session = getSession();
|
|
345
|
+
try {
|
|
346
|
+
const conv = await session.run(
|
|
347
|
+
`MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
|
|
348
|
+
{ conversationId, accountId }
|
|
349
|
+
);
|
|
350
|
+
if (conv.records.length === 0) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`cloudflare-task-tracker: conversationId=${conversationId.slice(0, 8)} has no :Conversation node \u2014 invariant violated, ensureConversation missed at session boot`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const conversationElementId = conv.records[0].get("id");
|
|
356
|
+
let messageElementId = null;
|
|
357
|
+
let raisedDuringTag = `raisedDuringConversation=${conversationId.slice(0, 8)}`;
|
|
358
|
+
if (messageId) {
|
|
359
|
+
const msgRow = await session.run(
|
|
360
|
+
`MATCH (m:Message {messageId: $messageId, accountId: $accountId, conversationId: $conversationId}) RETURN elementId(m) AS id LIMIT 1`,
|
|
361
|
+
{ messageId, accountId, conversationId }
|
|
362
|
+
);
|
|
363
|
+
if (msgRow.records.length > 0) {
|
|
364
|
+
messageElementId = msgRow.records[0].get("id");
|
|
365
|
+
raisedDuringTag = `raisedDuringMessage=${messageId.slice(0, 8)}`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const adminLabelStr = typeof inputs?.adminLabel === "string" ? inputs.adminLabel : "?";
|
|
369
|
+
const adminDomainStr = typeof inputs?.adminDomain === "string" ? inputs.adminDomain : "?";
|
|
370
|
+
const publicLabelStr = typeof inputs?.publicLabel === "string" ? inputs.publicLabel : "";
|
|
371
|
+
const publicDomainStr = typeof inputs?.publicDomain === "string" ? inputs.publicDomain : "";
|
|
372
|
+
const publicSegment = publicDomainStr ? `, public=${publicLabelStr}.${publicDomainStr}` : "";
|
|
373
|
+
const description = `Cloudflare setup for admin=${adminLabelStr}.${adminDomainStr}${publicSegment}`;
|
|
374
|
+
const props = {
|
|
375
|
+
taskId,
|
|
376
|
+
accountId,
|
|
377
|
+
name: "Cloudflare tunnel login + setup",
|
|
378
|
+
description,
|
|
379
|
+
status: "running",
|
|
380
|
+
priority: "normal",
|
|
381
|
+
kind: TASK_KIND,
|
|
382
|
+
inputsProvided,
|
|
383
|
+
startedAt: now,
|
|
384
|
+
createdAt: now,
|
|
385
|
+
updatedAt: now
|
|
386
|
+
};
|
|
387
|
+
if (inputs) {
|
|
388
|
+
if (!inputSchema) {
|
|
389
|
+
process.stderr.write(
|
|
390
|
+
`[task] redact-no-schema kind=${TASK_KIND} taskId=${taskId} fieldsCount=${Object.keys(inputs).length}
|
|
391
|
+
`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
const { redacted, droppedFields, droppedFieldNames } = (0, import_dist2.redactSecrets)(inputs, inputSchema);
|
|
395
|
+
for (const [key, value] of Object.entries(redacted)) {
|
|
396
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
397
|
+
props[`inputs.${key}`] = value;
|
|
398
|
+
}
|
|
399
|
+
if (droppedFields > 0) {
|
|
400
|
+
process.stderr.write(
|
|
401
|
+
`[task] redacted kind=${TASK_KIND} taskId=${taskId} droppedFields=${droppedFields} fields=${droppedFieldNames.join(",")}
|
|
402
|
+
`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const relationships = [
|
|
407
|
+
{
|
|
408
|
+
type: "RAISED_DURING",
|
|
409
|
+
direction: "outgoing",
|
|
410
|
+
targetNodeId: messageElementId ?? conversationElementId
|
|
411
|
+
}
|
|
412
|
+
];
|
|
413
|
+
const result = await (0, import_dist.writeNodeWithEdges)({
|
|
414
|
+
session,
|
|
415
|
+
labels: ["Task"],
|
|
416
|
+
props,
|
|
417
|
+
relationships,
|
|
418
|
+
createdBy: {
|
|
419
|
+
agent: CREATED_BY_AGENT,
|
|
420
|
+
session: conversationId,
|
|
421
|
+
tool: "cloudflare-setup-endpoint"
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
process.stderr.write(
|
|
425
|
+
`[task] action-start kind=${TASK_KIND} taskId=${taskId} ${raisedDuringTag}
|
|
426
|
+
`
|
|
427
|
+
);
|
|
428
|
+
return { taskId, taskElementId: result.nodeId };
|
|
429
|
+
} finally {
|
|
430
|
+
await session.close();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function appendCloudflareSteps(taskId, accountId, streamLogPath) {
|
|
434
|
+
if (!existsSync(streamLogPath)) return [];
|
|
435
|
+
let content;
|
|
436
|
+
try {
|
|
437
|
+
content = readFileSync(streamLogPath, "utf-8");
|
|
438
|
+
} catch {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
const steps = [];
|
|
442
|
+
for (const line of content.split(/\r?\n/)) {
|
|
443
|
+
const m = line.match(/\bphase_line\s+setup-tunnel\s+step=(\S+)/);
|
|
444
|
+
if (m) steps.push(m[1]);
|
|
445
|
+
}
|
|
446
|
+
if (steps.length === 0) return [];
|
|
447
|
+
const session = getSession();
|
|
448
|
+
try {
|
|
449
|
+
await session.run(
|
|
450
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})
|
|
451
|
+
SET t.steps = coalesce(t.steps, []) + $steps,
|
|
452
|
+
t.updatedAt = $updatedAt`,
|
|
453
|
+
{ taskId, accountId, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
454
|
+
);
|
|
455
|
+
for (const step of steps) {
|
|
456
|
+
process.stderr.write(
|
|
457
|
+
`[task] action-step kind=${TASK_KIND} taskId=${taskId} step=${step}
|
|
458
|
+
`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return steps;
|
|
462
|
+
} finally {
|
|
463
|
+
await session.close();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function completeCloudflareTask(params) {
|
|
467
|
+
const { taskId, taskElementId, accountId, conversationId, tunnelId, tunnelName, hostnames, status, errorMessage } = params;
|
|
468
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
469
|
+
if (status === "failed" && (!errorMessage || errorMessage.trim().length === 0)) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
"cloudflare-task-tracker: errorMessage is required when status='failed' (Task 885 process-provenance contract)."
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const session = getSession();
|
|
475
|
+
try {
|
|
476
|
+
if (status === "completed" && tunnelId && tunnelName) {
|
|
477
|
+
const conv = await session.run(
|
|
478
|
+
`MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
|
|
479
|
+
{ conversationId, accountId }
|
|
480
|
+
);
|
|
481
|
+
const conversationElementId = conv.records.length > 0 ? conv.records[0].get("id") : null;
|
|
482
|
+
const tunnelProps = {
|
|
483
|
+
accountId,
|
|
484
|
+
tunnelId,
|
|
485
|
+
tunnelName,
|
|
486
|
+
createdAt: now,
|
|
487
|
+
updatedAt: now
|
|
488
|
+
};
|
|
489
|
+
const tunnelRels = [
|
|
490
|
+
{ type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId }
|
|
491
|
+
];
|
|
492
|
+
if (conversationElementId) {
|
|
493
|
+
tunnelRels.push({
|
|
494
|
+
type: "RAISED_DURING",
|
|
495
|
+
direction: "outgoing",
|
|
496
|
+
targetNodeId: conversationElementId
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
const tunnelWrite = await (0, import_dist.writeNodeWithEdges)({
|
|
500
|
+
session,
|
|
501
|
+
labels: ["CloudflareTunnel"],
|
|
502
|
+
props: tunnelProps,
|
|
503
|
+
relationships: tunnelRels,
|
|
504
|
+
createdBy: {
|
|
505
|
+
agent: CREATED_BY_AGENT,
|
|
506
|
+
session: conversationId,
|
|
507
|
+
tool: "cloudflare-setup-endpoint"
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
for (const h of hostnames ?? []) {
|
|
511
|
+
const hostRels = [
|
|
512
|
+
{ type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId },
|
|
513
|
+
{
|
|
514
|
+
type: "ROUTES_TO",
|
|
515
|
+
direction: "outgoing",
|
|
516
|
+
targetNodeId: tunnelWrite.nodeId
|
|
517
|
+
}
|
|
518
|
+
];
|
|
519
|
+
await (0, import_dist.writeNodeWithEdges)({
|
|
520
|
+
session,
|
|
521
|
+
labels: ["CloudflareHostname"],
|
|
522
|
+
props: {
|
|
523
|
+
accountId,
|
|
524
|
+
hostnameValue: h.hostnameValue,
|
|
525
|
+
tunnelId,
|
|
526
|
+
isApex: h.isApex,
|
|
527
|
+
createdAt: now,
|
|
528
|
+
updatedAt: now
|
|
529
|
+
},
|
|
530
|
+
relationships: hostRels,
|
|
531
|
+
createdBy: {
|
|
532
|
+
agent: CREATED_BY_AGENT,
|
|
533
|
+
session: conversationId,
|
|
534
|
+
tool: "cloudflare-setup-endpoint"
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const setClauses = [
|
|
540
|
+
"t.status = $status",
|
|
541
|
+
"t.completedAt = $now",
|
|
542
|
+
"t.updatedAt = $now"
|
|
543
|
+
];
|
|
544
|
+
const queryParams = { taskId, accountId, status, now };
|
|
545
|
+
if (status === "failed" && errorMessage) {
|
|
546
|
+
setClauses.push("t.errorMessage = $errorMessage");
|
|
547
|
+
queryParams.errorMessage = errorMessage;
|
|
548
|
+
}
|
|
549
|
+
if (status === "completed") {
|
|
550
|
+
setClauses.push("t.errorMessage = null");
|
|
551
|
+
if (tunnelId) {
|
|
552
|
+
setClauses.push("t.tunnelId = $tunnelId");
|
|
553
|
+
queryParams.tunnelId = tunnelId;
|
|
554
|
+
}
|
|
555
|
+
if (tunnelName) {
|
|
556
|
+
setClauses.push("t.tunnelName = $tunnelName");
|
|
557
|
+
queryParams.tunnelName = tunnelName;
|
|
558
|
+
}
|
|
559
|
+
if (hostnames && hostnames.length > 0) {
|
|
560
|
+
setClauses.push("t.hostnames = $hostnamesList");
|
|
561
|
+
queryParams.hostnamesList = hostnames.map((h) => h.hostnameValue);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const updateRes = await session.run(
|
|
565
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})
|
|
566
|
+
SET ${setClauses.join(", ")}
|
|
567
|
+
RETURN size(coalesce(t.steps, [])) AS stepsCount, count(t) AS affected`,
|
|
568
|
+
queryParams
|
|
569
|
+
);
|
|
570
|
+
const stepsCount = updateRes.records[0]?.get("stepsCount")?.toNumber?.() ?? 0;
|
|
571
|
+
const affected = updateRes.records[0]?.get("affected")?.toNumber?.() ?? 0;
|
|
572
|
+
if (affected !== 1) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
`cloudflare-task-tracker: completeCloudflareTask MATCH count=${affected} for taskId=${taskId} \u2014 Task missing or accountId mismatch`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
process.stderr.write(
|
|
578
|
+
`[task] action-done kind=${TASK_KIND} taskId=${taskId} status=${status} stepsCount=${stepsCount}
|
|
579
|
+
`
|
|
580
|
+
);
|
|
581
|
+
} finally {
|
|
582
|
+
await session.close();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function findMostRecentTerminalCloudflareTask(accountId, conversationId) {
|
|
586
|
+
const session = getSession();
|
|
587
|
+
try {
|
|
588
|
+
const res = await session.run(
|
|
589
|
+
`MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})<-[:RAISED_DURING]-(t:Task {kind: $kind})
|
|
590
|
+
WHERE t.status IN ['completed', 'failed'] AND t.completedAt IS NOT NULL
|
|
591
|
+
RETURN t.taskId AS taskId, t.status AS status, t.completedAt AS completedAt, t.errorMessage AS errorMessage
|
|
592
|
+
ORDER BY t.completedAt DESC LIMIT 1`,
|
|
593
|
+
{ conversationId, accountId, kind: TASK_KIND }
|
|
594
|
+
);
|
|
595
|
+
if (res.records.length === 0) return null;
|
|
596
|
+
const row = res.records[0];
|
|
597
|
+
const taskId = row.get("taskId");
|
|
598
|
+
const status = row.get("status");
|
|
599
|
+
const completedAt = row.get("completedAt");
|
|
600
|
+
const errorMessage = row.get("errorMessage");
|
|
601
|
+
if (status !== "completed" && status !== "failed") return null;
|
|
602
|
+
return { taskId, outcome: status, completedAt, errorMessage };
|
|
603
|
+
} catch (err) {
|
|
604
|
+
process.stderr.write(
|
|
605
|
+
`[cf-task-tracker] findMostRecentTerminalCloudflareTask failed: ${err instanceof Error ? err.message : String(err)}
|
|
606
|
+
`
|
|
607
|
+
);
|
|
608
|
+
return null;
|
|
609
|
+
} finally {
|
|
610
|
+
await session.close();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function readTunnelState(brandConfigDir) {
|
|
614
|
+
const statePath = `${process.env.HOME ?? ""}/${brandConfigDir}/cloudflared/tunnel.state`;
|
|
615
|
+
if (!existsSync(statePath)) return null;
|
|
616
|
+
try {
|
|
617
|
+
const parsed = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
618
|
+
const tunnelId = typeof parsed.tunnelId === "string" ? parsed.tunnelId : null;
|
|
619
|
+
const tunnelName = typeof parsed.tunnelName === "string" ? parsed.tunnelName : null;
|
|
620
|
+
const domain = typeof parsed.domain === "string" ? parsed.domain : null;
|
|
621
|
+
if (!tunnelId || !tunnelName || !domain) return null;
|
|
622
|
+
return { tunnelId, tunnelName, domain };
|
|
623
|
+
} catch {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
var CLOUDFLARE_TASK_DIAGNOSTICS = {
|
|
628
|
+
scriptExitedNonzero: "script-exited-nonzero",
|
|
629
|
+
noTunnelStateOnDisk: "no-tunnel-state-on-disk",
|
|
630
|
+
endpointDiedPreReconcile: "endpoint-died-pre-reconcile"
|
|
631
|
+
};
|
|
632
|
+
function resolveUnitGoneVerdict(args) {
|
|
633
|
+
const { tunnelState, expected } = args;
|
|
634
|
+
const tunnelStateFound = tunnelState !== null;
|
|
635
|
+
const nameSet = typeof expected.tunnelName === "string" && expected.tunnelName.length > 0;
|
|
636
|
+
const idSet = typeof expected.tunnelId === "string" && expected.tunnelId.length > 0;
|
|
637
|
+
const identityMatch = tunnelState !== null && (nameSet && tunnelState.tunnelName === expected.tunnelName || idSet && tunnelState.tunnelId === expected.tunnelId);
|
|
638
|
+
if (tunnelStateFound && identityMatch) {
|
|
639
|
+
return { kind: "close-completed", tunnelStateFound: true, identityMatch: true };
|
|
640
|
+
}
|
|
641
|
+
return { kind: "close-failed", tunnelStateFound, identityMatch };
|
|
642
|
+
}
|
|
643
|
+
var RECONCILE_DELAY_BUDGET_MS = 9e4;
|
|
644
|
+
var RECONCILE_HARD_AGE_MS = 60 * 60 * 1e3;
|
|
645
|
+
async function recoverRunningCloudflareTasks(accountId, brandConfigDir, conversationIdForState) {
|
|
646
|
+
const session = getSession();
|
|
647
|
+
try {
|
|
648
|
+
const cutoff = new Date(Date.now() - RECONCILE_DELAY_BUDGET_MS).toISOString();
|
|
649
|
+
const stale = await session.run(
|
|
650
|
+
`MATCH (t:Task {accountId: $accountId, kind: $kind, status: 'running'})
|
|
651
|
+
WHERE t.startedAt < $cutoff
|
|
652
|
+
RETURN t.taskId AS taskId, elementId(t) AS taskElementId, t.startedAt AS startedAt
|
|
653
|
+
ORDER BY t.startedAt ASC`,
|
|
654
|
+
{ accountId, kind: TASK_KIND, cutoff }
|
|
655
|
+
);
|
|
656
|
+
const tunnelState = readTunnelState(brandConfigDir);
|
|
657
|
+
const tunnelStateFound = tunnelState !== null;
|
|
658
|
+
const resolved = [];
|
|
659
|
+
for (const record of stale.records) {
|
|
660
|
+
const taskId = record.get("taskId");
|
|
661
|
+
const taskElementId = record.get("taskElementId");
|
|
662
|
+
const startedAt = record.get("startedAt");
|
|
663
|
+
const ageMs = Date.now() - new Date(startedAt).getTime();
|
|
664
|
+
try {
|
|
665
|
+
if (tunnelStateFound && tunnelState) {
|
|
666
|
+
const tunnelExistsRow = await session.run(
|
|
667
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})-[:PRODUCED]->(tn:CloudflareTunnel {tunnelId: $tunnelId})
|
|
668
|
+
RETURN count(tn) AS existsCount`,
|
|
669
|
+
{ taskId, accountId, tunnelId: tunnelState.tunnelId }
|
|
670
|
+
);
|
|
671
|
+
const existsCountRaw = tunnelExistsRow.records[0]?.get("existsCount");
|
|
672
|
+
const tunnelAlreadyExists = (typeof existsCountRaw === "number" ? existsCountRaw : existsCountRaw?.toNumber?.() ?? 0) > 0;
|
|
673
|
+
if (tunnelAlreadyExists) {
|
|
674
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
675
|
+
const closeRes = await session.run(
|
|
676
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})
|
|
677
|
+
SET t.status = 'completed', t.completedAt = $now, t.updatedAt = $now,
|
|
678
|
+
t.errorMessage = null,
|
|
679
|
+
t.tunnelId = $tunnelId, t.tunnelName = $tunnelName
|
|
680
|
+
RETURN count(t) AS affected`,
|
|
681
|
+
{ taskId, accountId, now, tunnelId: tunnelState.tunnelId, tunnelName: tunnelState.tunnelName }
|
|
682
|
+
);
|
|
683
|
+
const aff = closeRes.records[0]?.get("affected");
|
|
684
|
+
const affN = typeof aff === "number" ? aff : aff?.toNumber?.() ?? 0;
|
|
685
|
+
if (affN !== 1) {
|
|
686
|
+
throw new Error(`reconciler close-out MATCH count=${affN} for taskId=${taskId}`);
|
|
687
|
+
}
|
|
688
|
+
resolved.push({ taskId, resolution: "completed-existing-tunnel" });
|
|
689
|
+
process.stderr.write(
|
|
690
|
+
`[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=true tunnel-already-written=true resolution=completed
|
|
691
|
+
`
|
|
692
|
+
);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
const convRow = await session.run(
|
|
696
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})-[:RAISED_DURING]->(target)
|
|
697
|
+
OPTIONAL MATCH (target)-[:PART_OF]->(c:Conversation)
|
|
698
|
+
RETURN coalesce(c.conversationId, target.conversationId) AS conversationId LIMIT 1`,
|
|
699
|
+
{ taskId, accountId }
|
|
700
|
+
);
|
|
701
|
+
const resolvedConversationId = convRow.records[0]?.get("conversationId");
|
|
702
|
+
await completeCloudflareTask({
|
|
703
|
+
taskId,
|
|
704
|
+
taskElementId,
|
|
705
|
+
accountId,
|
|
706
|
+
// Empty string when the edge resolution found no Conversation
|
|
707
|
+
// (rare — would require a malformed Task with no RAISED_DURING).
|
|
708
|
+
// The live close-out will produce a Tunnel without provenance
|
|
709
|
+
// edges in that case; better than silently dropping the close-out.
|
|
710
|
+
conversationId: resolvedConversationId ?? conversationIdForState ?? "",
|
|
711
|
+
tunnelId: tunnelState.tunnelId,
|
|
712
|
+
tunnelName: tunnelState.tunnelName,
|
|
713
|
+
hostnames: void 0,
|
|
714
|
+
status: "completed"
|
|
715
|
+
});
|
|
716
|
+
resolved.push({ taskId, resolution: "completed" });
|
|
717
|
+
process.stderr.write(
|
|
718
|
+
`[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=true tunnel-already-written=false resolution=completed
|
|
719
|
+
`
|
|
720
|
+
);
|
|
721
|
+
} else {
|
|
722
|
+
const diagnostic = ageMs > RECONCILE_HARD_AGE_MS ? CLOUDFLARE_TASK_DIAGNOSTICS.noTunnelStateOnDisk : CLOUDFLARE_TASK_DIAGNOSTICS.endpointDiedPreReconcile;
|
|
723
|
+
await completeCloudflareTask({
|
|
724
|
+
taskId,
|
|
725
|
+
taskElementId,
|
|
726
|
+
accountId,
|
|
727
|
+
conversationId: conversationIdForState ?? "",
|
|
728
|
+
status: "failed",
|
|
729
|
+
errorMessage: diagnostic
|
|
730
|
+
});
|
|
731
|
+
resolved.push({ taskId, resolution: `failed-${diagnostic}` });
|
|
732
|
+
process.stderr.write(
|
|
733
|
+
`[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=false resolution=failed errorMessage=${diagnostic}
|
|
734
|
+
`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
} catch (err) {
|
|
738
|
+
process.stderr.write(
|
|
739
|
+
`[task] action-recover kind=${TASK_KIND} taskId=${taskId} resolution=error reason="${err instanceof Error ? err.message : String(err)}"
|
|
740
|
+
`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return { scanned: stale.records.length, resolved };
|
|
745
|
+
} finally {
|
|
746
|
+
await session.close();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export {
|
|
751
|
+
openCloudflareTask,
|
|
752
|
+
appendCloudflareSteps,
|
|
753
|
+
completeCloudflareTask,
|
|
754
|
+
findMostRecentTerminalCloudflareTask,
|
|
755
|
+
readTunnelState,
|
|
756
|
+
CLOUDFLARE_TASK_DIAGNOSTICS,
|
|
757
|
+
resolveUnitGoneVerdict,
|
|
758
|
+
recoverRunningCloudflareTasks
|
|
759
|
+
};
|