@senpi/runtime 3.0.111 → 3.0.112
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/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +11 -0
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +70 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/auto-update/coordinator.d.ts +15 -1
- package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
- package/dist/runtime/auto-update/coordinator.js +53 -0
- package/dist/runtime/auto-update/coordinator.js.map +1 -1
- package/dist/runtime/auto-update/types.d.ts +14 -0
- package/dist/runtime/auto-update/types.d.ts.map +1 -1
- package/dist/runtime/auto-update/types.js.map +1 -1
- package/dist/runtime-api/intake/process-error-post.d.ts +6 -1
- package/dist/runtime-api/intake/process-error-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-error-post.js +5 -1
- package/dist/runtime-api/intake/process-error-post.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts +7 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +5 -0
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scaffold-tick-router.d.ts +39 -0
- package/dist/runtime-api/intake/scaffold-tick-router.d.ts.map +1 -0
- package/dist/runtime-api/intake/scaffold-tick-router.js +122 -0
- package/dist/runtime-api/intake/scaffold-tick-router.js.map +1 -0
- package/dist/runtime-api/intake/types.d.ts +49 -0
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.d.ts +2 -0
- package/dist/runtime-api/routes/signals.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.js +3 -0
- package/dist/runtime-api/routes/signals.js.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +42 -0
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +37 -0
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/runtime-api/server.d.ts +7 -1
- package/dist/runtime-api/server.d.ts.map +1 -1
- package/dist/runtime-api/server.js +1 -0
- package/dist/runtime-api/server.js.map +1 -1
- package/dist/scanners/external/scaffold/delivery.py +64 -17
- package/dist/scanners/external/scaffold/scaffold.py +128 -40
- package/dist/scanners/external/scaffold/senpi_mcp.py +88 -8
- package/dist/scanners/external/scaffold/tick_facts.py +130 -0
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +8 -1
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/skills-manager/manifest.d.ts.map +1 -1
- package/dist/skills-manager/manifest.js +1 -0
- package/dist/skills-manager/manifest.js.map +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -0
- package/dist/telemetry/live-telemetry.d.ts +54 -8
- package/dist/telemetry/live-telemetry.d.ts.map +1 -1
- package/dist/telemetry/live-telemetry.js +177 -7
- package/dist/telemetry/live-telemetry.js.map +1 -1
- package/dist/telemetry/scanner-tick-feed.d.ts +87 -0
- package/dist/telemetry/scanner-tick-feed.d.ts.map +1 -0
- package/dist/telemetry/scanner-tick-feed.js +48 -0
- package/dist/telemetry/scanner-tick-feed.js.map +1 -0
- package/dist/telemetry/tick-frame.d.ts +197 -0
- package/dist/telemetry/tick-frame.d.ts.map +1 -0
- package/dist/telemetry/tick-frame.js +347 -0
- package/dist/telemetry/tick-frame.js.map +1 -0
- package/dist/utils/telemetry-required-log.d.ts +13 -0
- package/dist/utils/telemetry-required-log.d.ts.map +1 -1
- package/dist/utils/telemetry-required-log.js +5 -1
- package/dist/utils/telemetry-required-log.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -23,10 +23,52 @@ export declare const SignalsBodySchema: import("@sinclair/typebox").TObject<{
|
|
|
23
23
|
marginPct: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
24
24
|
leverage: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
25
25
|
}>>;
|
|
26
|
+
tick: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
27
|
+
tick_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
28
|
+
status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
29
|
+
candidate_count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
30
|
+
duration_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
31
|
+
mcp_failures: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
32
|
+
mcp_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
33
|
+
mcp_calls: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
34
|
+
tool: import("@sinclair/typebox").TString;
|
|
35
|
+
ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
36
|
+
args: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
|
|
37
|
+
error_type: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
38
|
+
}>>>;
|
|
39
|
+
quiet_ticks: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
40
|
+
lag_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
41
|
+
journal_pending: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
42
|
+
commit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
43
|
+
state: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
|
|
44
|
+
state_fields: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
45
|
+
state_changed: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
46
|
+
}>>;
|
|
26
47
|
}>;
|
|
27
48
|
/** TypeBox schema for the /errors request body. */
|
|
28
49
|
export declare const ErrorsBodySchema: import("@sinclair/typebox").TObject<{
|
|
29
50
|
scanner_id: import("@sinclair/typebox").TString;
|
|
30
51
|
error: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>;
|
|
52
|
+
tick: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
53
|
+
tick_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
54
|
+
status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
55
|
+
candidate_count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
56
|
+
duration_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
57
|
+
mcp_failures: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
58
|
+
mcp_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
59
|
+
mcp_calls: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
60
|
+
tool: import("@sinclair/typebox").TString;
|
|
61
|
+
ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
62
|
+
args: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
|
|
63
|
+
error_type: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
64
|
+
}>>>;
|
|
65
|
+
quiet_ticks: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
66
|
+
lag_ms: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
67
|
+
journal_pending: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
68
|
+
commit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
69
|
+
state: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
|
|
70
|
+
state_fields: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
71
|
+
state_changed: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
72
|
+
}>>;
|
|
31
73
|
}>;
|
|
32
74
|
//# sourceMappingURL=signals.schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signals.schema.d.ts","sourceRoot":"","sources":["../../../src/runtime-api/routes/signals.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"signals.schema.d.ts","sourceRoot":"","sources":["../../../src/runtime-api/routes/signals.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgFH,yDAAyD;AACzD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;EAO5B,CAAC"}
|
|
@@ -38,14 +38,51 @@ const IntakeSignalSchema = Type.Object({
|
|
|
38
38
|
// No max on leverage — it is clamped downstream by the exchange.
|
|
39
39
|
leverage: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
|
|
40
40
|
}, { additionalProperties: false });
|
|
41
|
+
/**
|
|
42
|
+
* One MCP call inside a tick's facts. `args` is an open record like a signal's `data` is: it is
|
|
43
|
+
* author-supplied and cannot be enumerated here, and it is scrubbed and capped before it reaches
|
|
44
|
+
* any wire a browser reads.
|
|
45
|
+
*/
|
|
46
|
+
const IntakeTickCallSchema = Type.Object({
|
|
47
|
+
tool: Type.String({ minLength: 1 }),
|
|
48
|
+
ms: Type.Optional(Type.Number()),
|
|
49
|
+
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
50
|
+
error_type: Type.Optional(Type.String()),
|
|
51
|
+
}, { additionalProperties: false });
|
|
52
|
+
/**
|
|
53
|
+
* A tick's observability facts. Every field optional: a producer that sends none behaves exactly as
|
|
54
|
+
* before, and a producer that cannot measure one leaves it off rather than sending a zero, which
|
|
55
|
+
* would be read as a measurement.
|
|
56
|
+
*
|
|
57
|
+
* `additionalProperties: false` like the rest of the body — a key this box does not know is a
|
|
58
|
+
* producer/runtime mismatch worth a loud reject, not a silent drop.
|
|
59
|
+
*/
|
|
60
|
+
const IntakeTickSchema = Type.Object({
|
|
61
|
+
tick_id: Type.Optional(Type.String({ minLength: 1 })),
|
|
62
|
+
status: Type.Optional(Type.String()),
|
|
63
|
+
candidate_count: Type.Optional(Type.Number()),
|
|
64
|
+
duration_ms: Type.Optional(Type.Number()),
|
|
65
|
+
mcp_failures: Type.Optional(Type.Number()),
|
|
66
|
+
mcp_ms: Type.Optional(Type.Number()),
|
|
67
|
+
mcp_calls: Type.Optional(Type.Array(IntakeTickCallSchema)),
|
|
68
|
+
quiet_ticks: Type.Optional(Type.Number()),
|
|
69
|
+
lag_ms: Type.Optional(Type.Number()),
|
|
70
|
+
journal_pending: Type.Optional(Type.Number()),
|
|
71
|
+
commit: Type.Optional(Type.String()),
|
|
72
|
+
state: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
73
|
+
state_fields: Type.Optional(Type.Number()),
|
|
74
|
+
state_changed: Type.Optional(Type.Number()),
|
|
75
|
+
}, { additionalProperties: false });
|
|
41
76
|
/** TypeBox schema for the full /signals request body. */
|
|
42
77
|
export const SignalsBodySchema = Type.Object({
|
|
43
78
|
scanner_id: Type.String({ minLength: 1 }),
|
|
44
79
|
signals: Type.Array(IntakeSignalSchema),
|
|
80
|
+
tick: Type.Optional(IntakeTickSchema),
|
|
45
81
|
}, { additionalProperties: false });
|
|
46
82
|
/** TypeBox schema for the /errors request body. */
|
|
47
83
|
export const ErrorsBodySchema = Type.Object({
|
|
48
84
|
scanner_id: Type.String({ minLength: 1 }),
|
|
49
85
|
error: Type.Record(Type.String(), Type.Unknown()),
|
|
86
|
+
tick: Type.Optional(IntakeTickSchema),
|
|
50
87
|
}, { additionalProperties: false });
|
|
51
88
|
//# sourceMappingURL=signals.schema.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signals.schema.js","sourceRoot":"","sources":["../../../src/runtime-api/routes/signals.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEzC,iEAAiE;AACjE,MAAM,kBAAkB,GAAG,IAAI,CAAC,MAAM,CACpC;IACE,6EAA6E;IAC7E,8EAA8E;IAC9E,+EAA+E;IAC/E,sDAAsD;IACtD,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACxC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACpC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IACpE,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE;IAC1B,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE;IAC1B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAChD,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IACzC,iFAAiF;IACjF,iFAAiF;IACjF,kFAAkF;IAClF,mFAAmF;IACnF,6CAA6C;IAC7C,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IACrD,+EAA+E;IAC/E,iFAAiF;IACjF,0EAA0E;IAC1E,iBAAiB;IACjB,gFAAgF;IAChF,oFAAoF;IACpF,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IAC5E,iEAAiE;IACjE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAC;CAC9D,EACD,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAChC,CAAC;AAEF,yDAAyD;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC,MAAM,CAC1C;IACE,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACzC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"signals.schema.js","sourceRoot":"","sources":["../../../src/runtime-api/routes/signals.schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,7 +19,7 @@ import type { RuntimeHandle } from "../runtime/run.js";
|
|
|
19
19
|
import type { Logger } from "../utils/logger.js";
|
|
20
20
|
import type { ApiConfig } from "./config.js";
|
|
21
21
|
import type { IntakeContext } from "./intake/intake-context.js";
|
|
22
|
-
import type { IntakeSignal } from "./intake/types.js";
|
|
22
|
+
import type { IntakeSignal, IntakeTickFacts } from "./intake/types.js";
|
|
23
23
|
/**
|
|
24
24
|
* Dependencies passed to {@link createApi}.
|
|
25
25
|
*/
|
|
@@ -53,6 +53,12 @@ export interface CreateApiDeps {
|
|
|
53
53
|
* a no-op (which is what made the `/errors` boundary silent before B1 Task 2).
|
|
54
54
|
*/
|
|
55
55
|
onError?: (scannerId: string, error: Record<string, unknown>) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Called once per producer tick, from whichever of the two intake routes that tick took, with
|
|
58
|
+
* the facts the producer measured about the run itself. Feeds the live per-tick level and
|
|
59
|
+
* nothing else. Only used when `intakeContext` is present; defaults to a no-op.
|
|
60
|
+
*/
|
|
61
|
+
onTick?: (scannerId: string, tick: IntakeTickFacts) => void;
|
|
56
62
|
}
|
|
57
63
|
/**
|
|
58
64
|
* Artifacts returned by {@link createApi}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime-api/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAKxD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime-api/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAKxD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAIvE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC3C,4EAA4E;IAC5E,SAAS,EAAE,SAAS,CAAC;IACrB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtF;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;CAC7D;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAuF3D"}
|
|
@@ -100,6 +100,7 @@ export function createApi(deps) {
|
|
|
100
100
|
updateLiveness: (id, now) => deps.intakeContext.liveness.touch(id, now),
|
|
101
101
|
onAccepted: deps.onAccepted ?? (() => { }),
|
|
102
102
|
onError: deps.onError ?? (() => { }),
|
|
103
|
+
onTick: deps.onTick ?? (() => { }),
|
|
103
104
|
maxItemsPerSignalsRequest: deps.apiConfig.maxItemsPerSignalsRequest,
|
|
104
105
|
logger: deps.logger,
|
|
105
106
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/runtime-api/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,OAAiC,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAMjD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/runtime-api/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,8 +11,10 @@ build_delivery_sink(
|
|
|
11
11
|
clock: Callable[[], float] = time.time,
|
|
12
12
|
) -> sink
|
|
13
13
|
|
|
14
|
-
The returned sink is a callable: sink(candidates, tick_id=None)
|
|
15
|
-
|
|
14
|
+
The returned sink is a callable: sink(candidates, tick_id=None, tick=None)
|
|
15
|
+
`tick` is the scaffold's per-tick observability facts; the sink stamps the journal's
|
|
16
|
+
pending count onto them (it owns the journal) and rides them on the POST it already
|
|
17
|
+
makes. On each call it:
|
|
16
18
|
1. Replays journal on first call (if file-backed), re-queuing valid undelivered entries
|
|
17
19
|
2. Flushes still-valid undelivered entries from prior ticks (retry)
|
|
18
20
|
3. Wraps this tick's candidates into wire signals (skips invalid ones), stamping the
|
|
@@ -22,8 +24,10 @@ On each call it:
|
|
|
22
24
|
6. Interprets the response: accepted/expired/superseded/rejected = delivered;
|
|
23
25
|
transport failure or non-success envelope leaves entry queued for retry
|
|
24
26
|
|
|
25
|
-
sink.post_error(error: dict) -> None
|
|
26
|
-
POSTs {scanner_id, error} to <intake_url>/errors. Fire-and-forget.
|
|
27
|
+
sink.post_error(error: dict, tick: dict | None = None) -> None
|
|
28
|
+
POSTs {scanner_id, error} to <intake_url>/errors. Fire-and-forget. A failed tick's
|
|
29
|
+
facts ride here for the same reason they ride /signals on a clean one: this is the
|
|
30
|
+
POST that tick already makes.
|
|
27
31
|
|
|
28
32
|
Module constants:
|
|
29
33
|
SIGNALS_PATH = "/signals"
|
|
@@ -76,6 +80,10 @@ def build_delivery_sink(
|
|
|
76
80
|
# is not written again. Never cleared: an intake that refuses the field once would refuse it
|
|
77
81
|
# again, so re-arming would buy back the guaranteed-400 round-trip on every later tick.
|
|
78
82
|
intake_predates_tick_id = [False]
|
|
83
|
+
# The same latch for the body-level `tick` facts. Separate from the one above because
|
|
84
|
+
# they are separate fields on separate levels of the body: an intake may know one and
|
|
85
|
+
# not the other, and stripping both on either refusal would throw away what still works.
|
|
86
|
+
intake_predates_tick_facts = [False]
|
|
79
87
|
|
|
80
88
|
# ------------------------------------------------------------------
|
|
81
89
|
# Internal helpers
|
|
@@ -221,9 +229,23 @@ def build_delivery_sink(
|
|
|
221
229
|
for sig in signals
|
|
222
230
|
]
|
|
223
231
|
|
|
224
|
-
def
|
|
225
|
-
"""
|
|
232
|
+
def _with_pending(tick: dict | None) -> dict | None:
|
|
233
|
+
"""The tick facts plus the journal's BACKLOG, which only this closure knows.
|
|
234
|
+
|
|
235
|
+
Counted before this tick's own candidates are journalled, so the number is
|
|
236
|
+
signals from EARLIER ticks that the intake never acknowledged — a scanner
|
|
237
|
+
producing work that is not arriving. Counting this tick's own signals too
|
|
238
|
+
would put a 1 on every healthy signal-bearing tick and say nothing.
|
|
239
|
+
"""
|
|
240
|
+
if tick is None:
|
|
241
|
+
return None
|
|
242
|
+
return {**tick, "journal_pending": len(journal.pending())}
|
|
243
|
+
|
|
244
|
+
def _post_signals(signals: list[dict], tick: dict | None = None) -> tuple[int, Any]:
|
|
245
|
+
"""POST the signals list to the intake, with this tick's facts when it has any."""
|
|
226
246
|
body = {"scanner_id": scanner_id, "signals": signals}
|
|
247
|
+
if tick is not None and not intake_predates_tick_facts[0]:
|
|
248
|
+
body["tick"] = tick
|
|
227
249
|
# Log the exact wire shape so a 400 can be matched to the field that broke.
|
|
228
250
|
if signals:
|
|
229
251
|
log_event(
|
|
@@ -315,7 +337,7 @@ def build_delivery_sink(
|
|
|
315
337
|
# The sink callable
|
|
316
338
|
# ------------------------------------------------------------------
|
|
317
339
|
|
|
318
|
-
def _sink(candidates, tick_id=None) -> None:
|
|
340
|
+
def _sink(candidates, tick_id=None, tick=None) -> None:
|
|
319
341
|
tick_id = _usable_tick_id(tick_id)
|
|
320
342
|
current_tick_id[0] = tick_id
|
|
321
343
|
|
|
@@ -330,6 +352,10 @@ def build_delivery_sink(
|
|
|
330
352
|
# 1. Flush prior pending (retry candidates from older ticks)
|
|
331
353
|
prior_entries = _flush_prior_pending(now_ms)
|
|
332
354
|
|
|
355
|
+
# Read here and not below: the backlog is what EARLIER ticks left behind, and
|
|
356
|
+
# step 2 is about to journal this tick's own candidates on top of it.
|
|
357
|
+
tick_body = _with_pending(tick)
|
|
358
|
+
|
|
333
359
|
# 2. Wrap new candidates into signals
|
|
334
360
|
new_signal_pairs = _wrap_candidates(candidates, produced_at_ms, tick_id)
|
|
335
361
|
|
|
@@ -351,9 +377,10 @@ def build_delivery_sink(
|
|
|
351
377
|
all_signals.append(sig)
|
|
352
378
|
all_eids.append(eid)
|
|
353
379
|
|
|
354
|
-
# 3. POST (always, even when signals==[])
|
|
380
|
+
# 3. POST (always, even when signals==[]) — which is what makes this the channel
|
|
381
|
+
# the tick facts ride: a quiet scanner still posts, every tick.
|
|
355
382
|
try:
|
|
356
|
-
status, parsed = _post_signals(all_signals)
|
|
383
|
+
status, parsed = _post_signals(all_signals, tick_body)
|
|
357
384
|
except TransportError as exc:
|
|
358
385
|
log_event("delivery_transport_error", error=str(exc))
|
|
359
386
|
# All signals remain undelivered -> retry on next tick
|
|
@@ -367,12 +394,15 @@ def build_delivery_sink(
|
|
|
367
394
|
# the same batch without the field so one unknown key cannot strand real signals, and
|
|
368
395
|
# latch the discovery so later ticks leave the field off and stop paying the refused POST.
|
|
369
396
|
# Entries already journalled with an id still reach here and are still stripped per POST.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
):
|
|
373
|
-
if not intake_predates_tick_id[0]:
|
|
397
|
+
sent_tick_id = any(isinstance(sig, dict) and "tick_id" in sig for sig in all_signals)
|
|
398
|
+
sent_tick_facts = tick_body is not None and not intake_predates_tick_facts[0]
|
|
399
|
+
if _is_shape_reject(status, parsed) and (sent_tick_id or sent_tick_facts):
|
|
400
|
+
if sent_tick_id and not intake_predates_tick_id[0]:
|
|
374
401
|
intake_predates_tick_id[0] = True
|
|
375
402
|
log_event("delivery_intake_predates_tick_id", scanner_id=scanner_id)
|
|
403
|
+
if sent_tick_facts:
|
|
404
|
+
intake_predates_tick_facts[0] = True
|
|
405
|
+
log_event("delivery_intake_predates_tick_facts", scanner_id=scanner_id)
|
|
376
406
|
try:
|
|
377
407
|
status, parsed = _post_signals(_without_tick_id(all_signals))
|
|
378
408
|
except TransportError as exc:
|
|
@@ -386,15 +416,32 @@ def build_delivery_sink(
|
|
|
386
416
|
# even when the retry above had to send them without it.
|
|
387
417
|
_interpret_response(status, parsed, all_eids, all_signals)
|
|
388
418
|
|
|
389
|
-
def _post_error(error: dict) -> None:
|
|
390
|
-
"""POST an error dict
|
|
391
|
-
|
|
419
|
+
def _post_error(error: dict, tick: dict | None = None) -> None:
|
|
420
|
+
"""POST an error dict, with the failed tick's facts when the caller has them.
|
|
421
|
+
|
|
422
|
+
Fire-and-forget, with one exception: an intake that refuses the body because it
|
|
423
|
+
does not know `tick` is retried once without it and latched, so a failed tick's
|
|
424
|
+
error record is never lost to a field that only telemetry wanted.
|
|
425
|
+
"""
|
|
426
|
+
tick_body = _with_pending(tick)
|
|
392
427
|
url = intake_url.rstrip("/") + ERRORS_PATH
|
|
393
428
|
# The error belongs to exactly one tick and already names it; a body that
|
|
394
429
|
# names none leaves the key off rather than borrowing the current one.
|
|
395
430
|
origin = _tick_field(error.get("tick_id")) if isinstance(error, dict) else {}
|
|
431
|
+
|
|
432
|
+
def _body(with_tick: bool) -> dict:
|
|
433
|
+
body = {"scanner_id": scanner_id, "error": error}
|
|
434
|
+
if with_tick and tick_body is not None and not intake_predates_tick_facts[0]:
|
|
435
|
+
body["tick"] = tick_body
|
|
436
|
+
return body
|
|
437
|
+
|
|
396
438
|
try:
|
|
397
|
-
|
|
439
|
+
sent_tick_facts = tick_body is not None and not intake_predates_tick_facts[0]
|
|
440
|
+
status, parsed = post_json(url, _body(True), timeout)
|
|
441
|
+
if sent_tick_facts and _is_shape_reject(status, parsed):
|
|
442
|
+
intake_predates_tick_facts[0] = True
|
|
443
|
+
log_event("delivery_intake_predates_tick_facts", scanner_id=scanner_id)
|
|
444
|
+
post_json(url, _body(False), timeout)
|
|
398
445
|
except TransportError as exc:
|
|
399
446
|
log_event("delivery_error_post_failed", error=str(exc), **origin)
|
|
400
447
|
except Exception as exc: # noqa: BLE001
|
|
@@ -43,6 +43,7 @@ from .loop_primitives import (
|
|
|
43
43
|
log_event,
|
|
44
44
|
)
|
|
45
45
|
from .state_manager import StateManager
|
|
46
|
+
from .tick_facts import build_tick_facts, count_changed, project_state_row
|
|
46
47
|
from .ulid import new_ulid
|
|
47
48
|
|
|
48
49
|
|
|
@@ -105,22 +106,22 @@ def _build_senpi_mcp() -> Any:
|
|
|
105
106
|
)
|
|
106
107
|
|
|
107
108
|
|
|
108
|
-
def
|
|
109
|
-
"""Whether `
|
|
109
|
+
def _accepts_kwarg(fn: Any, name: str) -> bool:
|
|
110
|
+
"""Whether `fn` takes `name` as a keyword argument.
|
|
110
111
|
|
|
111
112
|
Read from the signature, once, rather than calling with the keyword and retrying on
|
|
112
113
|
TypeError: a sink that raises TypeError from its own body would then be called twice and
|
|
113
|
-
deliver the same candidates twice. A
|
|
114
|
-
|
|
114
|
+
deliver the same candidates twice. A callable whose signature cannot be read keeps the
|
|
115
|
+
narrower call it has today.
|
|
115
116
|
"""
|
|
116
117
|
try:
|
|
117
|
-
params = list(inspect.signature(
|
|
118
|
+
params = list(inspect.signature(fn).parameters.values())
|
|
118
119
|
except (TypeError, ValueError):
|
|
119
120
|
return False
|
|
120
121
|
return any(
|
|
121
122
|
p.kind == inspect.Parameter.VAR_KEYWORD
|
|
122
123
|
or (
|
|
123
|
-
p.name ==
|
|
124
|
+
p.name == name
|
|
124
125
|
and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
125
126
|
)
|
|
126
127
|
for p in params
|
|
@@ -260,7 +261,24 @@ def run_scaffold(
|
|
|
260
261
|
# The MCP boundary's per-tick failure tally, when the client exposes one. Duck-typed:
|
|
261
262
|
# an injected stub without it (a validation run) simply never reclassifies a tick.
|
|
262
263
|
mcp_tally = getattr(senpi_mcp, "tick_errors", None)
|
|
263
|
-
sink_takes_tick_id = sink is not None and
|
|
264
|
+
sink_takes_tick_id = sink is not None and _accepts_kwarg(sink, "tick_id")
|
|
265
|
+
# The per-tick observability facts ride the POST the sink already makes. A sink that
|
|
266
|
+
# predates them is called exactly as before, so an older sink is not broken by them.
|
|
267
|
+
sink_takes_tick = sink is not None and _accepts_kwarg(sink, "tick")
|
|
268
|
+
post_error_takes_tick = (
|
|
269
|
+
sink is not None
|
|
270
|
+
and hasattr(sink, "post_error")
|
|
271
|
+
and _accepts_kwarg(sink.post_error, "tick")
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Consecutive ticks that produced no candidate. 47 quiet ticks is information; the word
|
|
275
|
+
# "quiet" repeated 47 times is not, so the streak is counted here and sent as a number.
|
|
276
|
+
quiet_ticks = 0
|
|
277
|
+
# The previous tick's start, for the cadence lag. None on the first tick of a process:
|
|
278
|
+
# there is no gap to measure yet, and a 0 would claim the scanner was exactly on time.
|
|
279
|
+
prev_tick_started_at: Optional[float] = None
|
|
280
|
+
# The last projection reported, so `state_changed` says what MOVED rather than what exists.
|
|
281
|
+
prev_state_projection: Optional[dict] = None
|
|
264
282
|
|
|
265
283
|
try:
|
|
266
284
|
while not stop_event.is_set():
|
|
@@ -275,6 +293,14 @@ def run_scaffold(
|
|
|
275
293
|
# error it reports names the same tick.
|
|
276
294
|
tick_id = new_ulid()
|
|
277
295
|
tick_started_at = time.time()
|
|
296
|
+
# Actual gap between tick starts, minus the cadence the scanner is configured for.
|
|
297
|
+
# A positive number is a scanner falling behind its own interval.
|
|
298
|
+
lag_ms = (
|
|
299
|
+
None
|
|
300
|
+
if prev_tick_started_at is None
|
|
301
|
+
else int(round((tick_started_at - prev_tick_started_at - interval) * 1000))
|
|
302
|
+
)
|
|
303
|
+
prev_tick_started_at = tick_started_at
|
|
278
304
|
tick_status = "ok"
|
|
279
305
|
tick_error = None
|
|
280
306
|
candidates: list = []
|
|
@@ -365,7 +391,12 @@ def run_scaffold(
|
|
|
365
391
|
error_count=mcp_error_count,
|
|
366
392
|
)
|
|
367
393
|
|
|
394
|
+
# The quiet streak counts ticks, not words: a tick that produced a
|
|
395
|
+
# candidate ends the streak whatever its status.
|
|
396
|
+
quiet_ticks = 0 if candidates else quiet_ticks + 1
|
|
397
|
+
|
|
368
398
|
# --- Clean tick: commit ctx.state, deliver candidates ---
|
|
399
|
+
persist_error = None
|
|
369
400
|
if tick_status == "ok":
|
|
370
401
|
# Transactional commit. A failed persist RAISES (StateManager
|
|
371
402
|
# enforces it) and does NOT advance the committed snapshot — so
|
|
@@ -385,28 +416,75 @@ def run_scaffold(
|
|
|
385
416
|
error_type=type(commit_exc).__name__,
|
|
386
417
|
error=str(commit_exc),
|
|
387
418
|
)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
419
|
+
persist_error = {
|
|
420
|
+
"type": "state_persist_failed",
|
|
421
|
+
"tick": tick_count,
|
|
422
|
+
"tick_id": tick_id,
|
|
423
|
+
"message": (
|
|
424
|
+
"scanner state could not be persisted; "
|
|
425
|
+
"tick discarded, state not advanced"
|
|
426
|
+
),
|
|
427
|
+
}
|
|
428
|
+
commit_outcome = "committed" if committed else "persist_failed"
|
|
429
|
+
else:
|
|
430
|
+
# Non-ok tick (error / timeout / malformed): roll back the
|
|
431
|
+
# working buffer (discard the tick's mutations — state not
|
|
432
|
+
# advanced) and report to /errors via the sink, when supported.
|
|
433
|
+
ctx.state.rollback()
|
|
434
|
+
committed = False
|
|
435
|
+
commit_outcome = "rolled_back"
|
|
436
|
+
|
|
437
|
+
# Read AFTER the commit/rollback above, so this is always the last
|
|
438
|
+
# COMMITTED row — stale by one tick on a failed tick, which is exactly
|
|
439
|
+
# when the last state the scanner agreed on is worth seeing.
|
|
440
|
+
state_row = ctx.state.last()
|
|
441
|
+
state_projection = project_state_row(state_row)
|
|
442
|
+
state_fields = len(state_row) if isinstance(state_row, dict) else None
|
|
443
|
+
state_changed = (
|
|
444
|
+
None
|
|
445
|
+
if state_fields is None
|
|
446
|
+
else count_changed(state_projection, prev_state_projection)
|
|
447
|
+
)
|
|
448
|
+
prev_state_projection = state_projection if state_fields is not None else None
|
|
449
|
+
|
|
450
|
+
tick_facts = build_tick_facts(
|
|
451
|
+
tick_id=tick_id,
|
|
452
|
+
status=tick_status,
|
|
453
|
+
candidate_count=len(candidates),
|
|
454
|
+
duration_ms=duration_ms,
|
|
455
|
+
commit=commit_outcome,
|
|
456
|
+
quiet_ticks=quiet_ticks,
|
|
457
|
+
lag_ms=lag_ms,
|
|
458
|
+
mcp_snapshot=mcp_tally.snapshot() if mcp_tally is not None else None,
|
|
459
|
+
call_snapshot=(
|
|
460
|
+
mcp_tally.call_snapshot()
|
|
461
|
+
if mcp_tally is not None and hasattr(mcp_tally, "call_snapshot")
|
|
462
|
+
else None
|
|
463
|
+
),
|
|
464
|
+
state_row=state_row,
|
|
465
|
+
state_fields=state_fields,
|
|
466
|
+
state_changed=state_changed,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Exactly one POST carries this tick's facts: /signals on a clean tick,
|
|
470
|
+
# /errors on any tick that failed. Both already happen every tick, so the
|
|
471
|
+
# facts add no request of their own.
|
|
472
|
+
sink_can_post_error = sink is not None and hasattr(sink, "post_error")
|
|
473
|
+
facts_for_error = tick_facts if post_error_takes_tick else None
|
|
474
|
+
if tick_status == "ok":
|
|
475
|
+
if persist_error is not None and sink_can_post_error:
|
|
476
|
+
_post_error_safe(
|
|
477
|
+
sink, persist_error, tick_count, tick_id, facts_for_error
|
|
478
|
+
)
|
|
403
479
|
|
|
404
480
|
if committed and sink is not None:
|
|
405
481
|
try:
|
|
482
|
+
kwargs = {}
|
|
406
483
|
if sink_takes_tick_id:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
484
|
+
kwargs["tick_id"] = tick_id
|
|
485
|
+
if sink_takes_tick:
|
|
486
|
+
kwargs["tick"] = tick_facts
|
|
487
|
+
sink(candidates, **kwargs)
|
|
410
488
|
except Exception as sink_exc: # noqa: BLE001
|
|
411
489
|
log_event(
|
|
412
490
|
"scaffold_sink_error",
|
|
@@ -415,18 +493,14 @@ def run_scaffold(
|
|
|
415
493
|
error_type=type(sink_exc).__name__,
|
|
416
494
|
error=str(sink_exc),
|
|
417
495
|
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
tick_error or {"type": tick_status, "tick_id": tick_id},
|
|
427
|
-
tick_count,
|
|
428
|
-
tick_id,
|
|
429
|
-
)
|
|
496
|
+
elif sink_can_post_error:
|
|
497
|
+
_post_error_safe(
|
|
498
|
+
sink,
|
|
499
|
+
tick_error or {"type": tick_status, "tick_id": tick_id},
|
|
500
|
+
tick_count,
|
|
501
|
+
tick_id,
|
|
502
|
+
facts_for_error,
|
|
503
|
+
)
|
|
430
504
|
|
|
431
505
|
log_event(
|
|
432
506
|
"scaffold_tick_finished",
|
|
@@ -506,10 +580,24 @@ def _classify_mcp_errors(mcp_tally: Any, candidates: list) -> tuple[int, Optiona
|
|
|
506
580
|
}
|
|
507
581
|
|
|
508
582
|
|
|
509
|
-
def _post_error_safe(
|
|
510
|
-
|
|
583
|
+
def _post_error_safe(
|
|
584
|
+
sink: Any,
|
|
585
|
+
error: dict,
|
|
586
|
+
tick_count: int,
|
|
587
|
+
tick_id: str,
|
|
588
|
+
tick: Optional[dict] = None,
|
|
589
|
+
) -> None:
|
|
590
|
+
"""Call sink.post_error(error), swallowing/logging any failure.
|
|
591
|
+
|
|
592
|
+
`tick` is passed only when the sink's signature accepts it — the caller checks
|
|
593
|
+
once at launch, so a sink that predates the per-tick facts keeps its one-argument
|
|
594
|
+
call rather than being handed a keyword it would raise on.
|
|
595
|
+
"""
|
|
511
596
|
try:
|
|
512
|
-
|
|
597
|
+
if tick is None:
|
|
598
|
+
sink.post_error(error)
|
|
599
|
+
else:
|
|
600
|
+
sink.post_error(error, tick=tick)
|
|
513
601
|
except Exception as perr: # noqa: BLE001
|
|
514
602
|
log_event(
|
|
515
603
|
"scaffold_post_error_failed",
|